Pratham Patel 发布的文章

一份让你深入体验最新 Linux 内核编译过程的实操指南。

出于各种原因,自行编译 Linux 内核可能引起你的兴趣。这些原因可能包括但不限于:

  • 测试一个比你目前的 Linux 发行版更新的内核版本
  • 采用一组不同的配置选项、驱动来构建内核
  • 学习者的好奇心 ?

此指南将一步步指导你如何亲自编译 Linux 内核,包括你该运行哪些命令,为什么运行这些命令以及这些命令的执行效果。本文篇幅较长,所以请做好准备!

? 诸如 Ubuntu 这样的发行版提供了更简单地安装主线 Linux 内核的方式。但本教程目标是从源码手动完成所有工作。此教程需要你付出时间、耐心以及丰富的 Linux 命令行使用经验。本文更注重亲身实践的体验。不管怎么说,我仍建议你在虚拟机或备用系统中尝试此冒险,而非在你的主系统上进行。

前置准备

在软件领域,构建任何事物都有两个基本要求:

  1. 源代码
  2. 构建依赖

因此,作为预备环节,我们需要下载 Linux 内核的源码压缩包,并安装一些能让我们成功构建 Linux 内核的依赖项。

Linux 版本导览

在任何时刻,Freax Linux 内核都有四种“版本”。

Linux 的这些 “版本”,按照开发流程的顺序是:

  1. linux-next 树: 所有准备合并到 Linux 代码库的代码首先被合并到 linux-next 树。它代表的是 Linux 内核最新也是“最不稳定”的状态。大多数 Linux 内核开发者和测试人员使用这个来提高代码质量,为 Linus Torvalds 的后续提取做准备。请谨慎使用!
  2. 发布候选版(RC) / 主线版: Linus 从 linux-next 树抽取代码并创建一个初始发布版本。这个初始发布版本的测试版称为 RC( 发布候选 Release Candidate )版本。一旦 RC 版本发布,Linus 只会接受对它的错误修复和性能退化相关的补丁。基础这些反馈,Linus 会每周发布一个 RC 内核,直到他对代码感到满意。RC 发行版本的标识是 -rc 后缀,后面跟一个数字。
  3. 稳定版: 当 Linus 觉得最新的 RC 版本已稳定时,他会发布最终的“公开”版本。稳定发布版将会维护几周时间。像 Arch Linux 和 Fedora Linux 这样的前沿 Linux 发行版会使用此类版本。我建议你在试用 linux-next 或任何 RC 版本之前,先试一试此版本。
  4. LTS 版本: 每年最后一个稳定版将会再维护 几年。这通常是一个较旧的版本,但它会 会积极地维护并提供安全修复。Debian 的稳定版本会使用 Linux 内核的 LTS 版版本。

若想了解更多此方面的知识,可参阅 官方文档

本文将以当前可用的最新稳定版为例,编写此文时的 Linux 内核版本是 6.5.5

系统准备

由于 Linux 内核使用 C 语言编写,编译 Linux 内核至少需要一个 C 编译器。你的计算机上可能还需要其他一些依赖项,现在是安装它们的时候了。

? 这个指南主要聚焦于使用 GNU C 编译器(GCC)来编译 Linux 内核。但在未来的文章中(可能会深入介绍 Rust 的支持),我可能会介绍使用 LLVM 的 Clang 编译器作为 GCC 的替代品。

不过,请注意,MSVC 并不适用。尽管如此,我仍期待有微软的员工为此发送修补程序集。我在瞎想啥?

对于 Arch Linux 以及其衍生版本的用户,安装命令如下:

sudo pacman -S base-devel bc coreutils cpio gettext initramfs kmod libelf ncurses pahole perl python rsync tar xz

对于 Debian 以及其衍生版本的用户,安装命令如下:

sudo apt install bc binutils bison dwarves flex gcc git gnupg2 gzip libelf-dev libncurses5-dev libssl-dev make openssl pahole perl-base rsync tar xz-utils

对于 Fedora 以及其衍生版本的用户,安装命令如下:

sudo dnf install binutils ncurses-devel \
    /usr/include/{libelf.h,openssl/pkcs7.h} \
    /usr/bin/{bc,bison,flex,gcc,git,gpg2,gzip,make,openssl,pahole,perl,rsync,tar,xz,zstd}

下载 Linux 内核源码

请访问 kernel.org,在页面中寻找第一个 稳定 Stable 版本。你不会找不到它,因为它是最显眼的黄色方框哦 ?

点击访问 kernel.org

通过点击黄色的方框,你就可以下载 Tar 文件。同时,也别忘了下载相匹配的 PGP 签名文件,稍后我们需要用到它来验证 Tar 文件。它的扩展名为 .tar.sign

校验 Tar 文件的完整性

你如何知道刚下载的 Tar 文件是否被损坏?对于个人来说,一个损坏的 Tar 文件只会浪费你的宝贵时间,如果你是在为一个组织工作,那么可能会危及到组织的安全(这时你可能还有更大的问题需要担忧,但我们并不想让所有人都产生创伤后应激障碍!)。

为了验证我们的 Tar 文件的完整性,我们需要先解压它。目前,它是使用 XZ 压缩算法压缩的。因此,我将使用 unxz 工具(其实就是 xz --decompress 的别名)来解压 .tar.xz 格式的压缩文件。

unxz --keep linux-*.tar.xz

解压完成后,我们需要获取 Linus Torvalds 和 Greg KH 使用的 GPG 公开密钥。这些密钥用于对 Tar 文件进行签名。

gpg2 --locate-keys [email protected] [email protected]

你应该可以得到一个与我在我的电脑上看到的类似的结果:

$ gpg2 --locate-keys [email protected] [email protected]
gpg: /home/pratham/.gnupg/trustdb.gpg: trustdb created
gpg: key 38DBBDC86092693E: public key "Greg Kroah-Hartman <[email protected]>" imported
gpg: Total number processed: 1
gpg:               imported: 1
gpg: key 79BE3E4300411886: public key "Linus Torvalds <[email protected]>" imported
gpg: Total number processed: 1
gpg:               imported: 1
pub   rsa4096 2011-09-23 [SC]
      647F28654894E3BD457199BE38DBBDC86092693E
uid           [ unknown] Greg Kroah-Hartman <[email protected]>
sub   rsa4096 2011-09-23 [E]

pub   rsa2048 2011-09-20 [SC]
      ABAF11C65A2970B130ABE3C479BE3E4300411886
uid           [ unknown] Linus Torvalds <[email protected]>
sub   rsa2048 2011-09-20 [E]

在导入 Greg 和 Linus 的密钥后,我们可以使用 --verify 标志来验证 Tar 的完整性,操作如下:

gpg2 --verify linux-*.tar.sign

如果验证成功,你应该会看到如下的输出信息:

$ gpg2 --verify linux-*.tar.sign
gpg: assuming signed data in 'linux-6.5.5.tar'
gpg: Signature made Saturday 23 September 2023 02:46:13 PM IST
gpg:                using RSA key 647F28654894E3BD457199BE38DBBDC86092693E
gpg: Good signature from "Greg Kroah-Hartman <[email protected]>" [unknown]
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: 647F 2865 4894 E3BD 4571  99BE 38DB BDC8 6092 693E

务必查看是否存在 gpg: Good signature 的提示,然后再继续!

? 你可以忽略以下警告:WARNING: This key is not certified with a trusted signature! There is no indication that the signature belongs to the owner.

我们已根据 Linus 和 Greg 的邮件地址获取了公开密钥,并无需对此警告感到担忧。

解压 Tar 文件

如果你顺利的进行到这里,意味着你的 Tar 文件完整性检查已经成功完成。接下来,我们将从 Tar 文件中解压出 Linux 内核的源码。

The "TAR" xkcd comic: https://xkcd.com/1168/

这个步骤十分简单,只需对 Tar 文件执行 tar -xf 命令,如下:

tar -xf linux-*.tar

在这里,-x 选项表示解压,-f 选项则用来告诉 Tar 文件的文件名。

这个解压过程可能需要几分钟时间,你可以先放松,耐心等待一下。

配置 Linux 内核

Linux 内核的构建过程会查找 .config 文件。顾名思义,这是一个配置文件,用于指定 Linux 内核的所有可能的配置选项。这是必需的文件。

获取 Linux 内核的 .config 文件有两种方式:

  1. 使用你的 Linux 发行版的配置作为基础(推荐做法
  2. 使用默认的,通用的配置
? 也有第三种方法,也就是从零开始,手动配置每一个选项,但注意,这需要配置超过 12,000 个选项。并不推荐这种方式,因为手动配置所有选项将花费大量的时间,并且你还需要理解每个启用和禁用选项的含义。

使用发行版提供的配置

使用你的 Linux 发行版提供的配置是一个安全的选择。 如果你只是跟随这个指南测试一个不是你的发行版提供的新内核,那么这就是推荐的方式。

你的 Linux 发行版的 Linux 内核配置文件会在以下两个位置之一:

  • 大多数 Linux 发行版,如 Debian 和 Fedora 及其衍生版,将会把它存在 /boot/config-$(uname -r)
  • 一些 Linux 发行版,比如 Arch Linux 将它整合在了 Linux 内核中。所以,可以在 /proc/config.gz 找到。
? 如果两者都有,建议使用 /proc/config.gz。这是因为它在只读文件系统中,所以是未被篡改的。

进入含有已经解压出的 Tar 文件的目录。

cd linux-*/

接着,复制你的 Linux 发行版的配置文件:

### Debian 和 Fedora 及其衍生版:
$ cp /boot/config-"$(uname -r)" .config

### Arch Linux 及其衍生版:
$ zcat /proc/config.gz > .config
更新配置文件

一旦完成这些步骤,接下来就需要“更新”配置文件了。因为你的发行版提供的配置很可能比你正在构建的 Linux 内核版本要旧。

? 这同样适用于像 Arch Linux 和 Fedora 这样前沿的 Linux 发行版。 它们并不会因为有新版本可用就立刻发布更新。他们会进行一些质量控制工作,这必然会花费些时间。因此,即便是你的发行版提供的最新内核,相较于你在 kernel.org 上获取的版本也会滞后几个小版本。

要更新一个已有的 .config 文件,我们使用 make 命令搭配 olddefconfig 参数。简单解释一下,这个命令的意思是使用 旧的、默认的、配置

这将使用“旧的配置文件”(当前保存为 .config,这是你发行版配置的一份直接副本),并检查从上一版本以来 Linux 代码库中新加的任何配置选项。如果找到任何新的、未配置 的选项,该选项的默认配置值会被使用,并会对 .config 文件进行更新。

原来的 .config 文件将被重命名为 .config.old 进行备份,并将新的更改写入至 .config 文件。

make olddefconfig

以下是我机器上的输出:

$ file .config
.config: Linux make config build file, ASCII text

$ make olddefconfig
    HOSTCC  scripts/basic/fixdep
    HOSTCC  scripts/kconfig/conf.o
    HOSTCC  scripts/kconfig/confdata.o
    HOSTCC  scripts/kconfig/expr.o
    LEX     scripts/kconfig/lexer.lex.c
    YACC    scripts/kconfig/parser.tab.[ch]
    HOSTCC  scripts/kconfig/lexer.lex.o
    HOSTCC  scripts/kconfig/menu.o
    HOSTCC  scripts/kconfig/parser.tab.o
    HOSTCC  scripts/kconfig/preprocess.o
    HOSTCC  scripts/kconfig/symbol.o
    HOSTCC  scripts/kconfig/util.o
    HOSTLD  scripts/kconfig/conf
.config:8593:warning: symbol value 'm' invalid for USB_FOTG210_HCD
.config:8859:warning: symbol value 'm' invalid for USB_FOTG210_UDC
#
# configuration written to .config
#
针对 Debian 及其衍生版用户

Debian 及其衍生版为内核模块使用一个签名证书。默认情况下,你的计算机并不包含这个证书。

我推荐关闭启用模块签名的选项。具体如下所示:

./scripts/config --file .config --set-str SYSTEM_TRUSTED_KEYS ''
./scripts/config --file .config --set-str SYSTEM_REVOCATION_KEYS ''

如果你不这么做,在后面你进行 Linux 内核构建时,可能会导致构建失败。要注意这点。

使用自定义配置

如果你出于学习内核开发的目的学习如何构建 Linux 内核,那你应该这样做。

? 请注意,偏离你的 Linux 发行版的配置可能无法在实体硬件上“正常”工作。问题可能是特定硬件无法工作、Linux 内核无法启动等。

因此,我们只建议在虚拟机中使用。

你可以通过查看 make help 的输出 来查看 所有 可用的选项,但我们主要关注三个 make 目标:

  • defconfig: 默认配置。
  • allmodconfig: 根据当前系统状态,尽可能地把项目构建为可加载模块(而非内建)。
  • tinyconfig: 极简的 Linux 内核。

由于 tinyconfig 目标只会构建少数项目,构建时间将会缩短。我个人选择它的原因主要有:

  1. 检查我在代码/工具链中做的修改是否正确,以及代码是否可以编译。
  2. 在虚拟机中只进行少数选项的测试。

? 在为 ARM 或 RISC-V 机器构建 Linux 内核时,你可能需要 DTB(设备树的二进制文件)。使用 tinyconfig 目标将不会启用构建 DTB 的选项,你的内核很可能无法启动。

当然,你可以用 QEMU 在没有任何 DTB 的情况下启动 Linux 内核。但这篇文章并不会聚焦在此。或许你可以通过评论,让我在之后的时间里覆盖这个话题 ?

除非你确切地知道自己在做什么,否则你应当使用 defconfig 目标。 以下是我在我的电脑上运行的效果:

$ make defconfig
    HOSTCC  scripts/basic/fixdep
    HOSTCC  scripts/kconfig/conf.o
    HOSTCC  scripts/kconfig/confdata.o
    HOSTCC  scripts/kconfig/expr.o
    LEX     scripts/kconfig/lexer.lex.c
    YACC    scripts/kconfig/parser.tab.[ch]
    HOSTCC  scripts/kconfig/lexer.lex.o
    HOSTCC  scripts/kconfig/menu.o
    HOSTCC  scripts/kconfig/parser.tab.o
    HOSTCC  scripts/kconfig/preprocess.o
    HOSTCC  scripts/kconfig/symbol.o
    HOSTCC  scripts/kconfig/util.o
    HOSTLD  scripts/kconfig/conf
*** Default configuration is based on 'defconfig'
#
# configuration written to .config
#

修改配置

无论你是使用 Linux 发行版的配置并更新它,还是使用 defconfig 目标创建新的 .config 文件,你都可能希望熟悉如何修改这个配置文件。最可靠的修改方式是使用 menuconfignconfig 目标。

这两个目标的功能是相同的,只不过提供给你的界面有所不同。这是这两者间唯一的区别。我个人更偏向于使用 menuconfig 目标,但近来我发现 nconfig 在搜索选项时似乎更具直观性,所以我逐渐转向使用它。

首先,带着 menuconfig 目标运行 make 命令:

$ make menuconfig
    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

在此界面,你可以根据各选项的类型来进行切换操作。

有两类可切换选项:

  • 布尔状态选项:这类选项只能关闭([ ])或作为内建组件开启([*])。
  • 三态选项:这类选项可以关闭(< >)、内建(<*>),或作为可加载模块(<M>)进行构建。

想要了解更多关于某个选项的信息,使用上/下箭头键导航至该选项,然后按 <TAB> 键,直至底部的 < Help > 选项被选中,然后按回车键进行选择。此时就会显示关于该配置选项的帮助信息。

在修改选项时请务必谨慎。

当你满意配置后,按 <TAB> 键直到底部的 < Save > 选项被选中。然后按回车键进行选择。然后再次按回车键(记住,此时不要更改文件名),就能将更新后的配置保存到 .config 文件中。

构建 Linux 内核

构建 Linux 内核实际上十分简单。然而,在开始构建之前,让我们为自定义内核构建添加一个标签。我将使用字符串 -pratham 作为标签,并利用 LOCALVERSION 变量来实施。你可以使用以下命令实现配置:

./scripts/config --file .config --set-str LOCALVERSION "-pratham"

这一命令将 .config 文件中的 CONFIG_LOCALVERSION 配置选项设为我在结尾指定的字符串,即 -pratham。当然,你也不必非得使用我所用的名字哦 ?

LOCALVERSION 选项可用于设置一个“本地”版本,它会被附加到通常的 x.y.z 版本方案之后,并在你运行 uname -r 命令时一并显示。

由于我正在构建的是 6.5.5 版本内核,而 LOCALVERSION 字符串被设为 -pratham,因此,对我来说,最后的版本名将会是 6.5.5-pratham。这么做的目的是确保我所构建的自定义内核不会与发行版所提供的内核产生冲突。

接下来,我们来真正地构建内核。可以用以下的命令完成此步骤:

make -j$(nproc) 2>&1 | tee log

这对大部分(99%)用户来说已经足够了。

其中的 -j 选项用于指定并行编译任务的数量。而 nproc 命令用于返回可用处理单位(包括线程)的数量。因此,-j$(nproc) 其实意味着“使用我拥有的 CPU 线程数相同数量的并行编译任务”。

2>&1 会将 STDOUT 和 STDIN 重定向到相同的文件描述符,并通过管道传输给 tee 命令,这会将输出存储在一个名为 log 的文件,并且在控制台打印出完全相同的文本。如果你在构建时遇到错误,并希望回顾日志来检查出了什么问题,这将会十分有用。遇到那种情况,你只需要简单执行 grep Error log 命令就能找到线索。

自定义 make 目标

在 Linux 内核的源文件夹中,make 命令有一些自定义的目标可供执行各种操作。这些主要作为开发者的参考。如果你的唯一目标是安装一个比你当前发行版更新的 Linux 内核,那么你完全可以跳过这部分内容 ?

构建目标

作为一名开发者,你可能只想构建 Linux 内核,或者只想构建模块,或者只想构建设备树二进制(DTB)。在这种情况下,你可以指定一个构建目标,然后 make 命令只会构建指定的项目,而不会构建其他的。

以下是一些构建目标:

  • vmlinux:纯粹的 Linux 内核。
  • modules:可加载模块。
  • dtbs:设备树二进制文件(主要用于 ARM 和 RISC-V 架构)。
  • all:构建所有被标记了星号 * 的项目(从 make help 的输出中可以查看)。

通常情况下,你并不需要指定构建目标,因为它们都已经在构建列表中。所列出的目标是在你只想要测试某一个构建目标,而不是其他目标时的情况。

依据你的 计算机架构,构建完成的 Linux 内核镜像(存放在 /boot 目录)的名称会有所不同。

对于 x86_64,Linux 内核的默认镜像名称是 bzImage。因此,如果你只需要构建引导所需的 Linux 内核,你可以像下面这样设定 bzImage 为目标:

### 对于 x86_64
$ make bzImage

“那么如何在我的架构上找到用来调用 make 的目标名称呢?”

有两种方法。要么你可以执行 make help 之后查找在 Architecture specific targets 下,第一个前面带有星号 * 的选项。

或者,如果你希望自动完成,你可以利用 image_name 目标得到镜像的完全路径(相对路径),选择性地添加 -s 标志来获得有用的输出。

以下是我拥有的三台电脑的输出,一台是 x86_64,另一台是 AArch64,还有一台是 riscv

### x86_64
$ make -s image_name
arch/x86/boot/bzImage

### AArch64
$ make -s image_name
arch/arm64/boot/Image.gz

### RISC-V
$ make -s image_name
arch/riscv/boot/Image.gz

现在,要只构建 Linux 内核镜像,你可以这样进行:

make $(make -s image_name | awk -F '/' '{print $4}')
清理目标

如果你需要清理构建产生的文件,你可以用以下的目标来实现你的需求:

  • clean:除了 .config 文件外,删除几乎所有其他内容。
  • mrproper:执行了 make clean 的所有操作外,还会删除 .config 文件。
  • distclean:除了执行 make mrproper 的所有操作外,还会清理任何补丁文件。

安装

一旦成功编译了 Linux 内核,接下来就是启动安装一些东西的时候了。“一些 东西?” 没错,我们至少构建了两种不同的东西,如果你使用的是 ARM 或 RISC-V 架构,那就有三种。我会在以下内容中详细解释。

? 虽然我将告诉你不同的安装方式,尤其是关于如何改变默认安装路径的方法,但如果你不确定自己在做什么,那么我不建议你这么做! 请慎重考虑,如果你决定走自定义的路线,那你需要自己负责后果。默认设置之所以存在,是因为它们有其特殊的原因 ?

安装内核模块

Linux 内核有部分在系统启动时并非必需的。这些部分被构建为可加载模块,即在需要时才进行加载和卸载。

所以,首先需要安装这些模块。这可以通过 modules_install 目标完成。必须使用 sudo,因为模块会被安装在 /lib/modules/<kernel_release>-<localversion> 这个需要 root 权限的路径下。

这个过程不仅会安装内核模块,还会对其进行签名,所以可能需要一些时间。好消息是你可以通过之前提到的 -j$(nproc) 选项来并行执行安装任务,这样会快一些。?

sudo make modules_install -j$(nproc)

给开发者的提示: 你可以通过设定 INSTALL_MOD_PATH 变量来指定一个不同的路径存放 Linux 模块,而不用默认的 /lib/modules/<kernel_release>-<localversion>,具体如下:

   sudo make modules_install INSTALL_MOD_PATH=<path>
另一个给开发者的提示: 你可以使用 INSTALL_MOD_STRIP 变量来决定是否需要剥离模块的调试符号。如果未设定该变量,调试符号不会被剥离。当设为 1 时,符号信息将会被使用 --strip-debug 选项剥离,随后该选项会传递给 strip(或者在使用 Clang 的时候传递给 llvm-strip)工具。

(可选)安装 Linux 内核头文件

如果你打算使用这个内核来支持树外模块,比如 ZFS 或英伟达 DKMS,或者打算尝试自行编写模块,你可能会需要 Linux 内核提供的头文件。

可以通过以下方式使用 headers_install 目标来安装 Linux 内核头文件:

sudo make headers_install

应使用 sudo 命令,因为这些头文件会被安装到 /usr 目录。同时还会在 /usr 目录内创建子目录 include/linux,然后将头文件安装到 /usr/include/linux 内。

给开发者的提示: 通过设定 INSTALL_HDR_PATH 变量,你可以修改 Linux 内核头文件的安装路径。

安装 DTB(只针对 ARM 和 RISC-V)

如果你使用的是 x86\_64 架构,那么你可以跳过此步骤!

如果你针对 ARM 或者 RISC-V 构建了内核,那么在运行 make 的过程中,设备树的二进制文件可能已经被编译出来了。你可以通过在 arch/<machine_architecture>/boot/dts 目录查找 .dtb 文件来确认这一点。

这里提供了一个快速检查的技巧:

### 对于 AArch32
$ find arch/arm/boot/dts -name "*.dtb" -type f | head -n 1 > /dev/null && echo "DTBs for ARM32 were built"

### 对于 AArch64
$ find arch/arm64/boot/dts -name "*.dtb" -type f | head -n 1 > /dev/null && echo "DTBs for ARM64 were built"

### 对于 RISC-V
$ find arch/riscv/boot/dts -name "*.dtb" -type f | head -n 1 > /dev/null && echo "DTBs for RISC-V were built"

如果你看到出现 DTBs for <arch> were built 的消息,那么你可以开始安装 DTB。这可以通过 dtbs_install 目标来实现。

需要使用 sudo,因为它们会被安装在 /boot/dtb-<kernel_release>-<localversion> 中,而这个目录是由 root 所拥有的。

sudo make dtbs_install
给开发者的提示: 就像安装模块一样,你可以使用 INSTALL_DTBS_PATH 变量指定一个自定义的路径来安装设备树二进制文件。

安装 Linux 内核

最后,我们来安装 Linux 内核本身!这可以通过 install 目标来完成,就像这样:

sudo make install

在这里必须使用 sudo,因为 Linux 内核将被安装在 /boot 目录,而这个目录不允许普通用户写入。

? 一般来讲,install 目标也会更新引导加载程序,但是如果它没有成功,那可能是不支持你使用的引导加载程序。如果你没有使用 GRUB 作为你的引导加载程序,请一定要阅读你引导加载程序的使用手册 ?
给开发者的提示: 并不奇怪,INSTALL_PATH 变量被用来设定 Linux 内核的安装位置,而非默认的 /boot 目录。

针对 Arch Linux 用户的说明

如果你尝试执行了 make install 命令,可能已经注意到产生了错误。错误如下:

$ sudo make install
    INSTALL /boot
Cannot find LILO.

要在 Arch Linux 上实际完成 Linux 内核的安装,我们需要手动复制 Linux 内核镜像文件。别担心,如果你使用的是 Arch Linux,手动操作应该是家常便饭了。( ͡° ͜ʖ ͡°)

可以使用以下命令完成这个步骤:

sudo install -Dm644 "$(make -s image_name)" /boot/vmlinuz-<kernel_release>-<localversion>

因为我编译的是 6.5.5 版本的内核,所以我将会执行下面这条命令,你可以根据你的实际情况进行适当调整:

sudo install -Dm644 "$(make -s image_name)" /boot/vmlinuz-6.5.5-pratham

虽然不是必须的,但最好复制一份名为 System.map 的文件。既然你已经在操作了,一并也复制了 .config 文件吧 ?

sudo cp -vf System.map /boot/System.map-<kernel_release>-<localversion>
sudo cp -vf .config /boot/config-<kernel_release>-<localversion>
生成初始 RAM 磁盘

当你安装 Arch Linux 时,可能已经了解过 mkinitcpio 这个工具。现在,我们将使用它来创建初始的 RAM 磁盘。

首先,我们需要创建一个预设文件。向 /etc/mkinitcpio.d/linux-<localversion>.preset 文件中添加以下内容,根据实际需要来替换 <kernel_release><localversion>

ALL_config="/etc/mkinitcpio.conf"
ALL_kver="/boot/vmlinuz-<kernel_release>-<localversion>"

PRESETS=('default' 'fallback')

default_image="/boot/initramfs-<kernel_release>-<localversion>.img"
fallback_options="-S autodetect"

配置完成后,执行下面的命令来生成初始 RAM 磁盘:

sudo mkinitcpio -p linux-<localversion>

我自己的电脑上得到的输出如下,你的结果应该会类似!

$ sudo mkinitcpio -p linux-pratham
==> Building image from preset: /etc/mkinitcpio.d/linux-pratham.preset: 'default'
==> Using configuration file: '/etc/mkinitcpio.conf'
    -> -k /boot/vmlinuz-6.5.5-pratham -c /etc/mkinitcpio.conf -g /boot/initramfs-6.5.5-pratham.img
==> Starting build: '6.5.5-pratham'
    -> Running build hook: [base]
    -> Running build hook: [udev]
    -> Running build hook: [autodetect]
    -> Running build hook: [modconf]
    -> Running build hook: [kms]
    -> Running build hook: [keyboard]
==> WARNING: Possibly missing firmware for module: 'xhci_pci'
    -> Running build hook: [keymap]
    -> Running build hook: [consolefont]
==> WARNING: consolefont: no font found in configuration
    -> Running build hook: [block]
    -> Running build hook: [filesystems]
    -> Running build hook: [fsck]
==> Generating module dependencies
==> Creating zstd-compressed initcpio image: '/boot/initramfs-6.5.5-pratham.img'
==> Image generation successful
==> Building image from preset: /etc/mkinitcpio.d/linux-pratham.preset: 'fallback'
==> Using configuration file: '/etc/mkinitcpio.conf'
==> WARNING: No image or UKI specified. Skipping image 'fallback'

初始 RAM 磁盘已成功生成,现在我们可以进入下一步,更新引导加载器!

更新 GRUB

一旦所有必要的文件已成功复制到其对应的位置,接下来,我们将进行 GRUB 的更新。

使用以下命令对 GRUB 引导加载器进行更新:

sudo grub-mkconfig -o /boot/grub/grub.cfg
? 如果你使用的引导加载器不是 GRUB,请参看 Arch Wiki 中相关的引导加载器文档。

注意,更新 GRUB 并不会直接使新的内核版本设为默认启动选项。在引导时,请在启动菜单中手动选择新的内核版本。

你可以通过选择 Advanced options for Arch Linux 菜单,并在随后的菜单中选择 Arch Linux, with Linux <kernel_release>-<localversion> 来启用新版的 Linux 内核。

重启电脑

恭喜你!你已经完成了获取 Linux 内核源代码、进行配置、构建以及安装等所有步骤。现在只需要通过重启电脑并进入新构建和安装的 Linux 内核,就可以开始享受你的努力成果了。

启动时,请确保从引导加载器中选择正确的 Linux 内核版本。系统启动后,运行 uname -r 命令来确认你正在使用预期的 Linux 内核。

以下是我自己的电脑输出的内容:

$ uname -r
6.5.5-pratham

是时候开始庆祝了! ?

卸载操作

? 提示:在删除当前正在使用的内核版本之前,你应该首先切换至较旧的内核版本。

可能你的 Linux 发行版所使用的 Linux 内核版本就是你手动编译的版本,或者你自行编译了新的内核并注意到应卸载旧的内核以节省空间,于是你开始想如何才能卸载。当然,虽然我们无法简单地运行 make uninstall 命令,但这并不代表没有其他的方法!

我们清楚各个文件的安装位置,因此删除它们相对简单。

### 删除内核模块
$ rm -rf /lib/modules/<kernel_release>-<localversion>

### 删除设备树二进制文件
$ rm -rf /boot/dtb-<kernel_release>-<localversion>

### 删除 Linux 内核本身
$ rm -vf /boot/{config,System,vmlinuz}-<kernel_release>-<localversion>

总结

这个过程不是一次简单的旅程,是吧?但是现在,我们终于抵达了终点。我们一起学习了手动编译 Linux 内核的全过程,包括安装依赖、获取和验证源码、解压源码、配置 Linux 内核、构建内核以及安装内核。

如果你喜欢这个详细的步骤指南,请给我留言反馈。如果在操作过程中遇到问题,也欢迎提出,让我知道!

(题图:MJ/853481c5-87e3-42aa-8ace-e9ddfa232f75)


via: https://itsfoss.com/compile-linux-kernel/

作者:Pratham Patel 选题:lujun9972 译者:ChatGPT 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

当涉及到 CPU 的时候,有许多术语:AArch64、x86\_64、amd64、arm 等等。了解它们是什么以及它们之间的区别。

当你查看数据表或软件下载页面时是否被 ARMAArch64x86_64i386 等术语混淆?这些被称为 CPU 架构,我会帮你深入了解这个计算话题。

以下的表将为你总结每个字符串所代表的意义:

CPU 架构描述
x86_64 /x86/amd6464 位 AMD/英特尔 CPU 的别称
AArch64 /arm64/ARMv8/ARMv964 位 ARM CPU 的别称
i38632 位 AMD/英特尔 CPU
AArch32 /arm/ARMv1ARMv732 位 ARM CPU 的别称
rv64gc /rv64g64 位 RISC-V CPU 的别称
ppc64le64 位 PowerPC CPU,小端字节序存储

从左到右是使用该术语来描述 CPU 架构超过其右侧其他可选用术语的偏好。

从左到右是使用该术语描述 CPU 架构的优先级,使用左侧的而不是其右侧的其他可供选择的术语。

如果你像我一样是个极客,并想要更深入地解释,请继续阅读!

概述:CPU 架构

通常来说,我之前列出的术语是描述 CPU 架构的。但严格讲,它们被计算机工程师视为 CPU 的 指令集架构 Instruction Set Architecture (ISA)。

CPU 的指令集架构定义了 CPU 如何解析二进制代码中的 1 和 0。

这些 CPU 的 ISA 有几个主要的类别:

  • x86(AMD/英特尔)
  • ARM
  • RISC-V
  • PowerPC(IBM 仍在使用)

当然,还有更多种类的 CPU ISA,比如 MIPS、SPARC、DEC Alpha 等等。但我列出的这些至今仍然被广泛使用(以某种形式)。

上述列出的 ISA 主要根据 内存总线的宽度 分为至少两个子集。内存总线的宽度指的是 CPU 和 RAM 一次能传输的位数。内存总线有很多种宽度,但最常见的是 32 位和 64 位。

? 32 位的 CPU ISA 要么是已经过时的历史产物,被留下来要么只是为了支持旧的系统,要么只运用在微控制器中。可以说,所有新的硬件都已经是 64 位的了,特别是那些面向消费者的硬件。

x86(AMD/英特尔)

x86 CPU 的指令集架构主要源于英特尔,因为英特尔是最初搭配 8085 微处理器创建了它。8085 微处理器的内存总线宽度为 16 位。而后来,AMD 加入了这个领域,并且一直紧随英特尔的步伐,直到 AMD 创建出了自己的超集 64 位架构,超过了英特尔。

x86 架构的子集如下:

  • i386:如果你拥有的是 2007 年之前的 CPU,那么这可能就是你的 CPU 架构。它是现在使用的 AMD/英特尔的 x86 架构的 32 位“版本”。
  • x86_64/x86/amd64:这三个术语在不同的项目中可能会被交替使用。 但它们都是指 x86 AMD/英特尔架构的 64 位“版本”。无论如何,x86_64 这个字符串比 x86amd64 使用得更广泛(也更受欢迎)。例如,FreeBSD 项目称 64 位的 x86 架构为 amd64,而 Linux 和 macOS 则称之为 x86_64
? 由于 AMD 在创造 64 位 ISA 上超越了英特尔,所以一些项目(比如 FreeBSD)把 x86 的 64 位版本称为 amd64但更被广泛接受的术语还是 x86\_64

对于 CPU ISA,“x86” 这个字符串是一种特殊的情况。你要知道,在从 32 位的 x86(i386)到 64 位的 x86(x86_64)的过渡过程中,CPU 制造商确保了 CPU 能够运行 32 位 64 位指令。所以,有时你可能会看到 x86 也被用来意指“这款产品只能运行在 64 位的计算机上,但如果该计算机能运行 32 位指令,那么你也可以在它上面运行 32 位的用户软件”。

这种 x86 的模糊性——也就是诸如能同时运行 32 位代码的 64 位处理器——其主要用于和存在于运行在 64 位处理器上的,但是允许用户运行 32 位软件的操作系统,Windows 就通过这种被称作“兼容模式”的特性运用了这种方式。

汇总一下,由 AMD 和 英特尔 设计的 CPU 有两种架构:32 位的(i386)和 64 位的(x86_84)。

其它的英特尔

x86_64 ISA 实际上有几个子集。这些子集都是 64 位,但它们新添加了诸如 SIMD( 单指令多数据 Single Instruction Multiple Data )指令等功能。

  • x86_64-v1:这是大多数人都熟知的基础 x86_64 ISA。当人们谈论 x86_64 时,他们通常指的就是 x86_64-v1 ISA。
  • x86_64-v2:此版本新增了更多如 SSE3( 流式 SIMD 扩展版本 3 Streaming SIMD Extensions 3 )之类的指令扩展。
  • x86_64-v3:除了基础指令外,还新增了像 AVX( 高级矢量扩展 Advance Vector eXtensions )和 AVX2 等指令。这些指令可以使用高达 256 位宽的 CPU 寄存器!如果你能够有效利用它们,就能大规模并行处理计算任务。
  • x86_64-v4:这个版本在 x86_64-v3 ISA 的基础上,迭代了更多的 SIMD 指令扩展,比如 AVX256 和 AVX512。其中,AVX512 可以使用高达 512 位宽的 CPU 寄存器

ARM

ARM 不仅是一家为 CPU ISA 制定规范的公司,它也设计并授权给其他厂商使用其 CPU 内核,甚至允许其他公司使用 ARM CPU ISA 设计自己的 CPU 内核。(最后那句话听起来就像是个 SQL 查询似的!)

你可能因为如树莓派这类的 单板计算机 Single Board Computer )(SBC)听说过 ARM。但其实 ARM 的 CPU 还广泛应用于手机中。最近,苹果从使用 x86_64 处理器转向了在其笔记本和台式机产品中使用自家设计的 ARM 处理器。

就像任一种 CPU 架构一样,ARM 基于内存总线宽度也有两个子集。

官方认定的 32 位和 64 位 ARM 架构的名称分别是 AArch32AArch64。这里的 AArch 字符串代表 “ Arm 架构 Arm Architecture ”。这些是 CPU 执行指令时可切换的模式

实际符合 ARM 的 CPU ISA 的指令规范被命名为 ARMvX,其中 X 是规范版本的代表数字。目前为止,已经有九个主要的规范版本。规范 ARMv1ARMv7 定义了适用于 32 位 CPU 的架构,而 ARMv8ARMv9 是适用于 64 位 ARM CPU 的规范。(更多信息在此

? 每个 ARM CPU 规范又有进一步的子规范。例如 ARMv8,我们有 ARMv8-R、ARMv8-A、ARMv8.1-A、ARMv8.2-A、ARMv8.3-A、ARMv8.4-A、ARMv8.5-A、ARMv8.6-A、ARMv8.7-A、ARMv8.8-A 和 ARMv8.9-A。 其中 -A 表示“应用核心”,-R 表示“实时核心”。

你可能会觉得困惑,为什么在 AArch64 正式被 ARM 认定为 64 位 ARM 架构后,有些人仍然称其为 arm64。原因主要有两点:

  1. arm64 这个名称在 ARM 决定采用 AArch64 之前就已经广为人知了。(ARM 的一些官方文档也将 64 位的 ARM 架构称为 arm64…… ?)
  2. Linus Torvalds 对 AArch64 这个名称表示不满。 因此,Linux 的代码库主要将 AArch64 称为 arm64。然而,当你在系统中运行 uname -m 时,输出仍然是 aarch64

因此,对于 32 位 ARM CPU,你应该寻找 AArch32 这个字符串,但有时也可能是 armarmv7。相似的,对于 64 位 ARM CPU,你应该找 AArch64 这个字符串,但有时也可能会是 arm64ARMv8ARMv9

RISC-V

RISC-V 是 CPU 指令集架构(ISA)的一个开源规范。**但这并不意味着 CPU 自身是开源的!**这有点像以太网的情况。以太网规范是开源的,但你需付费购买网线、路由器和交换器。同样,RISC-V CPU 也要花钱购买。 ?

尽管如此,这并没有阻止人们创建并在开源许可下提供免费获取(设计上的获取,并非物理核心/SoC)的 RISC-V 核心。这是其中的一项尝试

? 总结一下:如果你在寻找运行于 RISC-V 消费级 CPU 上的软件,你应该寻找 “rv64gc” 这一字符串。这是许多 Linux 发行版所公认的。

像所有 CPU 架构一样,RISC-V 拥有 32 位和 64 位 CPU 架构。但由于 RISC-V 是非常新的描述 CPU ISA 的方式,大部分主流消费端或客户端的 CPU 核心一般都是 64 位的。大部分 32 位的设计都是微控制器,用于非常具体的用例。

它们的区别在于 CPU 的扩展。被称为 RISC-V CPU 的最低要求即实现“ 基本整数指令集 Base Integer Instruction Set ”(rv64i)。

下表列出了一些扩展及其描述:

扩展名称描述
rv64i64 位基本整数指令集(必须的
m乘法和除法指令
a原子指令
f单精度浮点指令
d双精度浮点指令
g别名;一组运行通用操作系统所需的扩展集(包括 imafd
c压缩指令

rv64i 这一字符串中,rv 表示 RISC-V,64 指的是 64 位 CPU 架构,而 i 指的是强制性的基本整数指令集扩展。 rv64i 之所以是一体的,因为即使 i 被认为是一种“扩展”,但它是必须的

约定俗成的,扩展名称按上述特定顺序排列。因此,rv64g 展开为 rv64imafd,而不是 rv64adfim

? 还有其他一些像 Zicsr 和 Zifencei 这样的扩展,它们位于 dg 扩展之间,但我故意不列出,以避免令你感到害怕。

因此,严格说来,(在写这篇文章的时候)rv64g 实际上是 rv64imafdZicsrZifencei恶魔般的笑声

PowerPC

PowerPC 曾是苹果、IBM 以及,摩托罗拉早期合作时代的一种流行 CPU 架构。在苹果转向英特尔的 x86 架构之前,它一直被应用于苹果的全部消费品产品线。

最初,PowerPC 采取的是大端字节序的内存排序。后来随着 64 位架构的引入,增加了使用小端字节排序的选项。这么做的目的是为了与英特尔的内存排序保持兼容(以防止软件错误),因为英特尔自始至终都一直采用的是小端字节序。有关字节序的更多内容,我可以唠叨很久,不过你可以通过阅读 这篇 Mozilla 的文档 来了解更多。

由于字节序在此也起到了一定的作用,PowerPC 共有三种架构:

  • powerpc:表示 32 位的 PowerPC 架构。
  • ppc64:表示拥有大端字节序内存排序的 64 位 PowerPC 架构。
  • ppc64le:表示拥有小端字节序内存排序的 64 位 PowerPC 架构。

目前,ppc64le 是被广泛使用的架构

结论

市面上有各种各样的 CPU 架构。对于每一种架构,都有 32 位和 64 位的子集。在现有的 CPU 中,我们可以找到 x86、ARM、RISC-V 和 PowerPC 等架构。

其中,x86 是最广泛和易于获取的 CPU 架构,因为英特尔和 AMD 都采取了这种架构。此外,ARM 提供的产品几乎在手机和易于获取的单板计算机中被独占使用。

RISC-V 正在努力使硬件更广泛地被使用。我就有一款带有 RISC-V CPU 的单板计算机。 ?

而 PowerPC 主要用于服务器,至少当前如此。

(题图:MJ/634ac7ea-b344-443a-b041-3bb3b31a956f)


via: https://itsfoss.com/arm-aarch64-x86_64/

作者:Pratham Patel 选题:lujun9972 译者:ChatGPT 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

CloudReady 日趋流行,尤其是在使用低端硬件的用户中。我决定了解一下它并分享我的发现。

什么是 CloudReady?

CloudReady 是一个基于谷歌的 Chromium OS 开源代码仓库的操作系统。Neverware 公司是 CloudReady 背后的组织,它开发的 CloudReady 可以部署到现有的 PC 和 Mac 上,由于其对硬件的要求极低,因此可以保证在上述硬件上的性能提升。大体上来说,CloudReady 把你的旧计算机变成了 Chromebook。2020 年底 Google 收购了 Neverware

在分享我的经验和想法之前,让我先进一步介绍以下它。

谁应该尝试 CloudReady?

CloudReady

CloudReady 主要是面向那些能够从使用类 Chromebook 设备中获益,但是已经在硬件设备上进行了投入的机构。下面是我想到的一些例子:

  • 经过 CloudReady 扩展的 Chromium OS 用户界面足够简单,用户从 macOS 或 Windows 转向使用 CloudReady 几乎不需要培训。
  • 更好的安全性,用户不会被安装充斥于 macOS 和 Windows 上的恶意软件。
  • Chromium OS 的硬件要求低,能够在老旧的硬件上良好运行。
  • 通过“ 谷歌管理控制台 Google Admin Console ”来管理计算机。
  • 简单初始安装过程。

下面是运行 CloudReady 的最低硬件要求:

  • CPU:任何 2008 年之后的 CPU (没有提及对 ARM 架构 CPU 的支持情况,暂且认为仅支持 x86 架构 CPU,即英特尔和 AMD 的 CPU)
  • 内存:2 GB 及以上
  • 存储:16GB 及以上
  • 具备访问 BIOS 或 UEFI 的权限——为了能够从 USB 安装程序启动

如果你不确定自己的上网本是否能够运行 CloudReady,Neverware 公司发布了能够运行 CloudReady 的认证机型清单。目前该清单已包含超过 450 种机型。你可以通过 这个官方链接来核查自己的机型

CloudReady 与 Chrome OS 有什么不同?

如果你的主要使用目的是以下场景,那么 CloudReady 能够满足你的需要:

  • 通过 “ Neverware 管理门户 Neverware Admin Portal ”(到 Google 完成收购为止)或 “ 谷歌管理控制台 Google Admin Console ” 来管理 CloudReady 设备。
  • 你的工作可以通过网页浏览器来完成(基于网络服务)。

当你了解到“CloudReady 是一个基于 Chrome OS 的操作系统”时,你一定认为它至少应该能够运行安卓应用。

遗憾的是,事实并非如此。开源的 Chromium OS 不支持安卓运行时框架/服务,因此在 CloudReady 中不可用。由于一些法律和技术上的原因,Neverware 并没有将安卓运行时添加到 CloudReady 中。

另一方面它也阻止你 侧载 side-loading APK,因为根本没有运行安卓应用的东西。

当我试图从 应用抽屉 app drawer 启动 Play Store 时,它在浏览器中打开了谷歌 Play Store 的网页。这真是个坏消息。然而得益于 CloudReady 是基于“ 面向网络 web focused”的操作系统的,我的 Chromium 浏览器扩展运行良好。

A screenshot of the app drawer in CloudReady with the Google Play Store app icon  along with Chrome Extensions as “Apps”

所以如果你想通过 CloudReady 把自己的旧笔记本变成一个非触屏的平板,那你就不太走运了。

为什么会有 CloudReady?

你也许会疑惑,既然已经有了 Chrome OS,为什么 Neverware 还要投入资源开发 CloudReady 这个 “克隆体” 呢?

仔细观察运行 Chrome OS 的设备,你就会发现它们都是预装设备。也就是说 Chrome OS 只适用于生产 Chromebook 的 OEM 厂商。

对于微软的 Windows,OEM 厂商可以预装 Windows,用户也可以下载单独的 ISO。然而,谷歌并不提供可用于在电脑上安装 Chrome OS 的 ISO。

所以需要开发一个基于 Chromium OS 的操作系统,让你能够将其安装到已有的 PC 或 Mac 上。

CloudReady 为你提供一种安装基于 Chromium OS,企业用户也可以通过 Neverware 获得官方支持服务。

获取 CloudReady

CloudReady screenshot

CloudReady 提供三个版本:家庭版(免费)、教育版(付费)和企业版(付费)。如果你想先体验一下 CloudReady,那就选家庭版。

Neverware 不提供 ISO 镜像,但它提供一个启动 U 盘制作工具。这个工具仅限 Windows 操作系统。

Neverware 同时提供一个 RAW 文件,你可以用 任何基于 Chromium 的浏览器Chromebook 恢复扩展 来制作启动 U 盘。

下载 CloudReady 家庭版

如果你想要在虚拟机里体验 CloudReady 的话,Neverware 提供了 “.ova” 文件。该 “.ova” 文件无法在 VirtualBox 上使用,它旨在与 VMware 一起使用。

下载 CloudReady “.ova” 文件

Ubuntu Web:ChromeOS 和 CloudReady 的替代品?

如果你想要在旧电脑上使用 CloudReady,但是它缺少安卓运行时这点让你失望了,也许你可以试试 Ubuntu Web

A screenshot of Ubuntu Web

正如其名称所示,Ubuntu Web 是面向寻找 Chrome OS 替代品的人群的 Linux 发行版。

Ubuntu Web 有与 Ubuntu 一样的同步能力,让你能够用 /e/ Cloud(一个专注于隐私的 Google 云同步服务替代品)实现同步。

最重要的是 Ubuntu Web 默认附带了 Waydroid

Waydroid 是一种“基于容器的方式,可以在 GNU/Linux 系统上运行一个完整的 Android 系统”。也就是说你可以在 Ubuntu Web 上运行安卓应用(不像 CloudReady)。

总结

尽管你可能会觉得 CloudReady 与 Chrome OS 相比并没有太多的优势,但对于那些想要部署集中管理的、基于 Chromium OS 的操作系统,但又不想在 Chromebook 上投资的组织来说,它似乎是一个不错的选择。

对于使用低端硬件的家庭用户来说,它也是一个不错的选择,但是我们 已经有很多轻量级的 Linux 发行版

你是否已经使用过 CloudReady?或者你是第一次在这里听说它?你对这个项目的总体看法是什么呢?

(题图:MJ/5c18795b-6978-48a0-a6f7-baffde69ab48)


via: https://itsfoss.com/cloudready/

作者:Pratham Patel 选题:lujun9972 译者:toknow-gh 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

红帽公司将其源代码放在付费墙后面的最新决定,今天可能会对其直接竞争对手造成一定伤害,但对红帽公司自身而言,这将对之后产生负面影响。

红帽关闭源代码

基于我对 RHEL 和其他红帽产品的热爱,我不得不现在表达一些严厉的看法。之所以这样做,是因为我希望 RHEL [及/或其克隆版] 对每个人都是 可获得的

请原谅这次我用词有些激烈,因为我从内心不希望 RHEL 成为 对业余爱好者不可及 的产品。免费的 RHEL 开发者许可证只是转移话题而已,相比于 真正的 RHEL 克隆版存在的时候,它削弱了 广大动手爱好者社区 的热情。

?️ 对于我的使用场景来说,使用免费订阅的 RHEL 使我感到满意。实际上,我正在使用的就是免费订阅。本文是关于如果保持这种状态会对 RHEL 产生怎样的影响。

快速回顾:事情是如何走到这一步的

我相信你现在都已经知道为什么红帽公司最近频频出现在新闻中:

但是,如果你对正在发生的事情一无所知,让我给你一个快速时间线的指引,帮助你理解:

  1. 红帽公司拥有一个出色的 Linux 发行版,名为 红帽企业 Linux Red Hat Enterprise Linux (RHEL),支持周期长达 10 年。
  2. 与其他任何 Linux 发行版一样,用于构建 RHEL(或 RHEL 的 克隆版)的源代码是公开可用的。
  3. CentOS 利用上述源代码,去除商标,并创建了一个几乎 1 比 1 的 RHEL 拷贝。(我说“几乎”是因为 RHEL 的商标 必须 被删除。)
  4. 那些不愿意支付费用只是为了第一次尝试 RHEL 的爱好者社区,现在可以使用 CentOS 来看看是否喜欢它。
  5. 这个人中的一部分人随后向他们的高层管理人员介绍了 RHEL 及其 通过 CentOS 初步体验 到的卓越表现,他们很可能转向在 RHEL 上支持他们的产品或在部署中使用 RHEL。
  6. 由于 CentOS 是免费的,爱好者社区得以扩大。像 Jeff Geerling 这样的人使用 CentOS 来教授对 扮演系统管理员感兴趣 的新一代爱好者,让他们了解他的 开源存储库 中诸如 Ansible 之类的很棒的内容。
  7. CentOS 的二进制文件在 RHEL 更新后几周才发布。因此,红帽收购了 CentOS,以确保 CentOS 不会落后于 RHEL 的更新。
  8. 几年后,CentOS Stream 的惨淡出场,导致了 CentOS 的消亡。
  9. Rocky LinuxAlma Linux 出现填补了 CentOS 留下的空白。它们使用的是公开可用的 RHEL 源代码。
  10. 红帽公司开始限制源代码的访问,只允许其付费客户以及任何拥有免费的开发者订阅的人访问。
? 我特意没有提到 IBM 收购红帽的事情,因为如果红帽的某个人有勇气在 官方场合 公开说,“只是简单地重建代码,没有增加价值或以任何方式改变代码,对开源公司来说都是真正的威胁。”,他们肯定也可以直承 IBM 参与了这些决定。但几乎每个红帽员工都公开否认了这种指责。所以我也不相信 IBM 自己搬起石头砸自己的脚。但你可以根据自己的心来决定是否相信。
不,当我说 IBM 可能没有参与这个决定时,我并不是在讽刺。他们 有可能参与,但我不这么认为。

亲爱的红帽公司,你刚刚做了什么?

你向社区提供一款免费的产品(CentOS)。然后你在提供这个免费产品的过程中(CentOS)改变它的的支持周期,并将其“替代品”(CentOS Stream)用作你闪亮的企业产品(RHEL)的“测试场”。

然后那些水蛭们拿起你闪亮产品的源代码,创建了一个 CentOS 的接替者(Rocky Linux 和 Alma Linux)。你不喜欢这样,所以对它们进行了 “软付费墙” 的限制。

现在,虽然 你有权利这样做(因为你从为 RHEL 提供支持而不是 RHEL 本身赚钱),但我会解释为什么这对 你自己来说 是一个糟糕的举动。

  • 问题: RHEL 在企业环境中为什么如此受欢迎?
  • 答案: CentOS Stream
  • 问题: 在线教程用什么来教授 RHEL?
  • 答案: CentOS Stream
  • 问题: 那些跟随上述在线教程的学习者用什么来学习 RHEL?
  • 答案: CentOS Stream
  • 问题: 当这些学生成为老师时,他们会向询问如何入门 Linux 系统管理员的人推荐什么?
  • 答案: CentOS Stream
  • 问题: 如果有人愿意购买 RHEL 的许可证,但因为没有公开的软件仓库而犹豫不决,他们会用什么?
  • 答案: CentOS Stream

简而言之,如果红帽公司继续对 RHEL 的克隆产品制造麻烦,以下是可能出现的情况:

  • 许多参与企业部署的客户和专业人士将考虑放弃 RHEL,并且不再对其提供支持。
  • 新用户将开始考虑使用 Ubuntu、Debian、openSUSE 或其他长期存在的替代产品。
  • 大学和 IT 培训也将转向替代方案,例如 Ubuntu 或 openSUSE,而不再纠结于 RHEL 克隆、CentOS Stream 和 Fedora 之间。

此外,根据他们的 FAQ,小型企业和大学无法使用免费的红帽开发者订阅:

无费用的、无支持的红帽开发者订阅是为个人和个人帐户设计的。

除非红帽公司友好对待 RHEL 克隆产品,否则我无法再看到这个生态系统中会有任何新的参与者了。 这实在令人沮丧,因为 RHEL 是一款出色的产品。是的,它可能不像 Fedora 那样前沿,但是使用起来仍然很有乐趣!

我会引用 Brian Stevens 的 一句话 来阐述观点:

我们的信仰核心是,当有共同目标或问题的人们可以自由地联结和合作时,他们汇集起来的创新可以改变世界。我们相信开源的开发过程能够产生更好的代码,而用户社区会创造出一个让代码具有影响力的受众。

RHEL 是一款企业级发行版,因此红帽公司几乎不会致力于为在树莓派上运行 RHEL 提供支持。猜猜是哪些发行版为树莓派提供了企业级 Linux 镜像。提示一下,它们是 Rocky LinuxAlma Linux。我敢打赌,红帽公司并没有统计出有多少人使用 RHEL 是因为他们先在树莓派上尝试使用 Rocky/Alma Linux,然后转而使用 RHEL。我就是其中之一(使用免费的 RHEL 许可证,也就那点价值)。

那么对于 RHEL 来说,这意味着什么?我不是 先知(此处双关 “Oracle”,哈哈!),所以无法预测 RHEL 的未来。

我远不是一个“引领潮流者”,也不知道有多少人受到我 关于 Podman 的报道 的积极影响。尝试使用 Podman 并非没有原因。我先在 Fedora 上试用,然后通过使用 Rocky Linux 在一个“生产级”环境中进行 大量实验,最后在 RHEL 上部署了一些我自己的服务。这并不是说“如果我没有这么做,其他人就不会这样做”,但你不能否认,从我和 许多其他人 这样的多方共同努力中产生的内容所带来的 影响。顺便说一下,是我促使 Abhishek 去报道 Podman,而不是相反。

当然,上述提到的“贡献”有些可能对红帽公司没有帮助,但它们对于 红帽公司的客户群体 来说是有帮助的。

所以,我们应该感到担忧吗?也是,也不是。

红帽公司做出的决定,从商业角度来看是有道理的。但也不完全是。从短期目标来看是有道理的,但从长期来看则不然。

红帽公司作为一家自豪地向上游贡献的公司,他们不会停止继续贡献。即使通过“软付费墙”来限制 RHEL 的代码,红帽公司仍将继续向上游贡献。红帽公司将继续在 新的 发展进行 创新

他们只是不再像以前那样将 RHEL 的“秘密配方”(在你期望的意义上)开放了。这个秘密的配方本身并不是“专有的”。红帽在 RHEL 中提供的几乎所有东西都是开源的。

他们的秘密在于 将补丁向后移植到 RHEL 稳定包 中。将这些补丁仅提供给 RHEL 的客户,这是一个非常公平的 商业决策。这些补丁也可以公开用于同一软件包的 不同版本。将补丁应用于使“旧版本”软件包保持最新状态的任务非常困难。

所以,我理解他们为什么做出这个决定。

红帽并没有将 RHEL 变为闭源(至少从技术上来说)。

红帽依然是一家出色的公司,其拥有经过验证的开源产品组合。我每天都在使用其中一些产品,比如:在树莓派 4 上运行 RHEL(是的,这是可能的!)、(无需 root 的)Podman、Cockpit、Ansible、systemd 等等!

以下你应该感到担忧的原因

如果 RHEL 不是自由提供的 —— 不是指免费,而是像 Debian、Ubuntu 甚至 Fedora 那样无需账户即可下载 —— 那么对于想要进入企业 Linux 生态系统的新人来说,他们的数量将继续减少。我认为是这样……

如果这个数量减少了,实际上推荐企业使用和支付 RHEL 的人数也会减少。你知道这会引发什么样的循环。

  • 对 RHEL 感兴趣的新人变少 → 购买的 RHEL 订阅变少
  • 红帽的收入减少 → 对上游项目的贡献减少(如 systemd、Podman、Linux 内核、GNOME、Wayland、英伟达合作等)
  • 最终 → Linux 生态系统的总体改进变少

当然,红帽并不是唯一向 Linux 生态系统做出贡献的公司,但你不能否认它对推动生态系统全面向前 流动 所产生的巨大影响!

我对红帽声明的回应

✋ 我并不是在攻击 Mike McGrath。这只是对他的陈述的直接回应。我相信作为一个 RHEL 用户,我有权利表达自己的观点,因为我非常喜欢它,以至于通过树莓派 4B 上的 RHEL 部署了个人博客。

除非另有说明,以下所有引文均摘自 这篇博文

引文 1

我感觉大部分对我们近期对下游源代码的决策所引发的愤怒,大部分来自 那些不愿意为 RHEL 所付出时间、精力和资源买单的人,或者那些想要将其重新打包以谋取自己利益的人。

是的,这完全公平,但我要一直强调这一点,直到你意识到,如果没有广大社区在 CentOS 及其后续产品上的培训,RHEL 就什么都不是。

现在的 IT 专业人员中包括曾经在这个群体中的人,而你现在给他们打上了 “那些不愿意为所付出时间...”的标签。 ?

通过展示这样的立场,你会让更少的人接触企业级 Linux,并且之后使用 RHEL 的人数显著减少。

引文 2

我们必须为从事这项工作的人支付报酬 —— 那些在漫长的工作时间和夜晚中辛勤工作、相信开源价值观的热情贡献者。简单地将这些个人产生的代码重新打包并原样转售,而没有增加任何价值,会导致这个开源软件的生产不可持续。这包括关键的向后移植工作和 上游正在开发的未来功能和技术。如果这项工作变得不可持续,它将停止,这对任何人来说都不好。

这不仅是一个完全合理的观点,而且也是残酷的现实。开源软件的资金不足。当任何人都可以无需付出实际回报就使用你的产品时,为开源软件筹集资金也变得非常困难。

但是,如果社区没有免费获得 RHEL(在限制源代码之前),那么它可能不会像今天这样取得巨大的成功。社区通过自由探索 RHEL,并使其成为更为壮大。

我不是让红帽公司做慈善事业,只是白白地免费提供。我希望红帽公司有足够的资金来改进上游。但是应该在 某个地方 找到一种折中方案。

再次强调,免费订阅并不等同于 RHEL 的克隆。当然,我写过关于如何 免费获取红帽企业版 Linux 的文章。然而,这并不是相同的精神。我在这里 引用 Jeff Geerling 的话:

“不,请不要说‘但你可以使用你的红帽开发者订阅!’我在 Debian、Ubuntu、Arch 等系统上可以不使用它。你明白我的意思。”

引文 3

最近,我们已经确定,拥有一个下游的重构者没有价值。

绝对是有价值的!我不会在这里重复我是如何通过 RHEL 的重构版本进入 RHEL 的这一点。

Windows 之所以受欢迎,只是因为微软允许盗版存在(和更便宜的许可证密钥)。

如果他们加强了控制,没有一个家庭会在 Vista 的继任者上花一分钱。我并不是要将 Vista(一款灾难性的操作系统)与 RHEL 进行比较,但这个类比大多数人应该有共鸣,你可以想想没有得到 Windows 许可证退款时的那种不甘。

引文 4

通常公认的观点是,这些免费重构就是产生 RHEL 专家并转化为销售的渠道,这并不是现实情况。我希望我们生活在那样的世界,但实际情况并非如此。相反,我们发现了一群用户,其中许多用户属于大型或非常大型的 IT 组织,他们希望获得 RHEL 的稳定性、周期和硬件生态系统,而无需实际支持维护者、工程师、编写人员和许多其他角色。

...你确定吗? ?

当然,从免费用户转化为 RHEL 客户的转化率可能不是很高,但我敢打赌,这个比例不会低于 30%。如果你坚持这样做,新客户的数量将不到当前新“注册”用户的 10%。

对于利用免费的 RHEL 开发者许可证也可以提出同样的论点。对于那些不愿意支付 RHEL 费用的人来说,使用临时电子邮件 ID 创建新的红帽账户以便利用免费的开发者订阅是一项不容忽视的任务。

当我们谈论转化率时,有多少专业人士正是使用 RHEL 的克隆版本进行内部产品开发(以便与 RHEL 进行适配),但实际上为使用 RHEL 的客户提供支持呢?

你如何衡量由第三方支持提供给客户的价值? 你不能,它不是一种可以衡量的有形物质。

我想扯远了,但这确实是真实发生的事件。我目前所使用的 [小镇当地的] ISP 使用 RHEL 对用户进行身份验证,因为他从一个向他出租互联网线路的公司获得了一个可以在 CentOS 和 RHEL 上部署的身份验证产品。

猜猜他们在内部是用什么开发来与 RHEL 对接? ?

引文 5

仅仅是重建代码,不添加价值,也不以任何方式进行更改,对开源公司来说是一个真正的威胁。这是对开源的真正威胁,这可能将开源重新变成只有业余爱好者和黑客参与的活动。

那么像 Rocky Linux 和 Alma Linux 这样的 RHEL 克隆做的工作还不够重要,不足以与“仅仅重建代码,不添加价值,也不以任何方式进行更改”相区分吗?

? 我不知道如何更礼貌地重新表述这句话,所以请不要往心里去。这来自于对 RHEL 的热爱,而不是对红帽(管理层)的误导性愤怒。
  • Rocky LinuxAlma Linux 都为树莓派提供镜像,这使得在沉浸于企业级 Linux 这个隐喻的幸福海洋之前,可以更容易、更便宜地试水。
  • Alma Linux 有一个名为 Elevate 的工具,允许 所有用户(这显然包括 RHEL,甚至是 Alma 的“竞争对手” Rocky Linux)升级到主要版本(例如从 7.x 到 8.x,等等)。我相信红帽公司的客户会喜欢这样的工具。
  • Rocky Linux 有一个名为 Peridot 的构建工具。它使任何人都可以拥有一个自定义构建的 RHEL。这个构建可以基于 Rocky Linux,或者甚至可以是公司的内部 RHEL 克隆,以防止供应链攻击。

这个回答也涉及到前面引用中的以下子引用:

仅仅将这些个人创造的代码重新打包,并原样转售,而不添加任何价值,会导致这个开源软件的生产无法持续下去。

它们正在为企业 Linux 生态系统增加价值,而不是直接为 RHEL 增加价值。

关于 CentOS Stream……

CentOS Stream 是一个奇怪的产品。在任何意义都不算糟糕(至少我个人是这么认为的),但是很奇怪。它与 RHEL 在以下关键方面存在差异,其中之一肯定会成为某些人的绝对禁忌:

  • 它是 RHEL 的测试组,这使得对于 RHEL 和特别兴趣小组(SIG)来说更加便利,但是对于你的使用情况而言,可能会有帮助,也可能没有。
  • CentOS Stream 不使用数字版本号的命名方案。这样一个微小的变化,在 CentOS Stream 上的测试时,可能会破坏为 RHEL 设计的脚本。同样的情况也可能出现在安全更新方面,正如 一位 Twitter 用户所描述的
  • 正如我之前提到的,安全修复在 CentOS Stream 上发布较晚。虽然这可能有一些原因,但对于依靠 RHEL 克隆版本的小企业来说,并没有帮助,因为它们仍然容易受到威胁。

结论

现在 CentOS Stream 已经存在,RHEL 的 开发 比以往更加开放。但巧合的是,RHEL 的源代码 却被置于一个软性付费墙之后,受制于 Red Hat 的最终用户许可协议(EULA)

目前这可能对红帽有所帮助。但如果保持这种立场,甚至不需要采取其他措施打击 RHEL 克隆产品,我相信围绕企业级 Linux 的用户、开发者和支持社区将会逐渐衰落...

我认为从长远来看,这将会伤害到红帽,因为随着周围的社区消亡,只有疲惫不堪的 IT 专业人员(意思是:被迫使用它的人,就像使用 Oracle 产品一样)才会继续使用 RHEL,甚至更糟糕的是,使用 Oracle Linux。

我在这里并不是在抨击红帽的员工。而是希望让你知道,如果继续这样发展下去,热爱 RHEL 的社区将会消亡。

我不愿意看到那种局面 ?

我真心地祝愿红帽在推动 Linux 生态系统前进方面一切顺利!真心的 ❤️

✍️ 此观点来自 Pratham Patel,一位热爱红帽的开源爱好者和探索者。

via: https://news.itsfoss.com/red-hat-fiasco/

作者:Pratham Patel 选题:lkxed 译者:ChatGPT 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

到目前为止,我们已经讲解了包括 变量、可变性、常量数据类型函数if-else 语句循环 在内的一些关于 Rust 编程的基础知识。

在 Rust 基础系列的最后一章里,让我们现在用 Rust 编写一个程序,使用这些主题,以便更好地理解它们在现实世界中的用途。让我们来编写一个相对简单的程序,用来从水果市场订购水果。

我们程序的基本结构

来让我们首先向用户问好,并告诉他们如何与程序交互。

fn main() {
    println!("欢迎来到水果市场!");
    println!("请选择要购买的水果。\n");

    println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
    println!("购买完成后,请输入“quit”或“q”。\n");
}

获取用户输入

上面的代码非常简单。目前,你不知道接下来该做什么,因为你不知道用户接下来想做什么。

所以让我们添加一些代码,接受用户输入并将其存储在某个地方以便稍后解析,然后根据用户输入采取适当的操作。

use std::io;

fn main() {
    println!("欢迎来到水果市场!");
    println!("请选择要购买的水果。\n");

    println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
    println!("购买完成后,请输入“quit”或“q”。\n");

    // 获取用户输入
    let mut user_input = String::new();
    io::stdin()
        .read_line(&mut user_input)
        .expect("无法读取用户输入。");
}

有三个新元素需要告诉你。所以让我们对这些新元素进行浅层次的探索。

1. 理解 use 关键字

在这个程序的第一行,你可能已经注意到我们“使用”(哈哈!)了一个叫做 use 的新关键字。Rust 中的 use 关键字类似于 C/C++ 中的 #include 指令和 Python 中的 import 关键字。使用 use 关键字,我们从 Rust 标准库 std 中“导入”了 io(输入输出)模块。

LCTT 译注:“使用”在原文中为“use”,与新介绍的关键字一样。

你可能会想知道为什么我们在可以使用 println 宏来将某些内容输出到标准输出时,导入 io 模块是必要的。Rust 的标准库有一个叫做 prelude 的模块,它会自动被包含。该模块包含了 Rust 程序员可能需要使用的所有常用函数,比如 println 宏。(你可以在 这里 阅读更多关于 std::prelude 模块的内容。)

Rust 标准库 std 中的 io 模块是接受用户输入所必需的。因此,我们在程序的第一行添加了一个 use 语句。

2. 理解 Rust 中的 String 类型

在第 11 行,我创建了一个新的可变变量 user_input,正如它的名字所表示的那样,它将被用来存储用户输入。但是在同一行,你可能已经注意到了一些“新的”东西(哈哈,又来了!)。

LCTT 译注:“新的”在原文中为“new”,在第 11 行的代码中,原作者使用了 String::new() 函数,所以此处的梗与“使用”一样,原作者使用了一个在代码中用到的单词。

我没有使用双引号("")声明一个空字符串,而是使用 String::new() 函数来创建一个新的空字符串。

""String::new() 的区别是你将在 Rust 系列的后续文章中学习到的。现在,只需要知道,使用 String::new() 函数,你可以创建一个可变的,位于堆上的字符串。

如果我使用 "" 创建了一个字符串,我将得到一个叫做“字符串切片”的东西。字符串切片的内容也位于堆上,但是字符串本身是不可变的。所以,即使变量本身是可变的,作为字符串存储的实际数据是不可变的,需要被覆盖而不是修改。

3. 接受用户输入

在第 12 行,我调用了 std::iostdin() 函数。如果我在程序的开头没有导入 std::io 模块,那么这一行将是 std::io::stdin() 而不是 io::stdin()

sdtin() 函数返回一个终端的输入句柄。read_line() 函数抓住这个输入句柄,然后,正如它的名字所暗示的那样,读取一行输入。这个函数接受一个可变字符串的引用。所以,我传入了 user_input 变量,通过在它前面加上 &mut,使它成为一个可变引用。

⚠️ read_line() 函数有一个 怪癖。这个函数在用户按下回车键之后 停止 读取输入。因此,这个函数也会记录换行符(\n),并将一个换行符存储在你传入的可变字符串变量的结尾处。

所以,请在处理它时要么考虑到这个换行符,要么将它删除。

Rust 中的错误处理入门

最后,在这个链的末尾有一个 expect() 函数。让我们稍微偏题一下,来理解为什么要调用这个函数。

read_line() 函数返回一个叫做 Result 的枚举。我会在后面的文章中讲解 Rust 中的枚举,但是现在只需要知道,枚举在 Rust 中是非常强大的。这个 Result 枚举返回一个值,告诉程序员在读取用户输入时是否发生了错误。

expect() 函数接受这个 Result 枚举,并检查结果是否正常。如果没有发生错误,什么都不会发生。但是如果发生了错误,我传入的消息(无法读取用户输入。)将会被打印到 STDERR,程序将会退出

? 所有我简要提及的新概念将会在后续的新 Rust 系列文章中讲解。

现在我希望你应该已经理解了这些新概念,让我们添加更多的代码来增加程序的功能。

验证用户输入

我接受了用户的输入,但是我没有对其进行验证。在当前的上下文中,验证意味着用户输入了一些“命令”,我们希望能够处理这些命令。目前,这些命令有两个“类别”。

第一类用户可以输入的命令是用户希望购买的水果的名称。第二个命令表示用户想要退出程序。

我们的任务现在是确保用户输入不会偏离 可接受的命令

use std::io;

fn main() {
    println!("欢迎来到水果市场!");
    println!("请选择要购买的水果。\n");

    println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
    println!("购买完成后,请输入“quit”或“q”。\n");

    // 获取用户输入
    let mut user_input = String::new();
    io::stdin()
        .read_line(&mut user_input)
        .expect("无法读取用户输入。");

    // 验证用户输入
    let valid_inputs = ["苹果", "香蕉", "橘子", "芒果", "葡萄", "quit", "q"];
    user_input = user_input.trim().to_lowercase();
    let mut input_error = true;
    for input in valid_inputs {
        if input == user_input {
            input_error = false;
            break;
        }
    }
}

要使验证更容易,我创建了一个叫做 valid_inputs 的字符串切片数组(第 17 行)。这个数组包含了所有可以购买的水果的名称,以及字符串切片 qquit,让用户可以传达他们是否希望退出。

用户可能不知道我们希望输入是什么样的。用户可能会输入“Apple”、“apple”或 “APPLE” 来表示他们想要购买苹果。我们的工作是正确处理这些输入。

在第 18 行,我通过调用 trim() 函数从 user_input 字符串中删除了尾部的换行符。为了处理上面提到的问题,我使用 to_lowercase() 函数将所有字符转换为小写,这样 “Apple”、“apple” 和 “APPLE” 都会变成 “apple”。

现在,来看第 19 行,我创建了一个名为 input_error 的可变布尔变量,初始值为 true。稍后在第 20 行,我创建了一个 for 循环,它遍历了 valid_inputs 数组的所有元素(字符串切片),并将迭代的模式存储在 input 变量中。

在循环内部,我检查用户输入是否等于其中一个有效字符串,如果是,我将 input_error 布尔值的值设置为 false,并跳出 for 循环。

处理无效输入

现在是时候处理无效输入了。这可以通过将一些代码移动到无限循环中来完成,如果用户给出无效输入,则 继续 该无限循环。

use std::io;

fn main() {
    println!("欢迎来到水果市场!");
    println!("请选择要购买的水果。\n");

    let valid_inputs = ["苹果", "香蕉", "橘子", "芒果", "葡萄", "quit", "q"];

    'mart: loop {
        let mut user_input = String::new();

        println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
        println!("购买完成后,请输入“quit”或“q”。\n");

        // 读取用户输入
        io::stdin()
            .read_line(&mut user_input)
            .expect("无法读取用户输入。");
        user_input = user_input.trim().to_lowercase();

        // 验证用户输入
        let mut input_error = true;
        for input in valid_inputs {
            if input == user_input {
                input_error = false;
                break;
            }
        }

        // 处理无效输入
        if input_error {
            println!("错误: 请输入有效的输入");
            continue 'mart;
        }
    }
}

这里,我将一些代码移动到了循环内部,并重新组织了一下代码,以便更好地处理循环的引入。在循环内部,第 31 行,如果用户输入了一个无效的字符串,我将 continue mart 循环。

对用户输入做出反应

现在,所有其他的状况都已经处理好了,是时候写一些代码来让用户从水果市场购买水果了,当用户希望退出时,程序也会退出。

因为你也知道用户选择了哪种水果,所以让我们问一下他们打算购买多少,并告诉他们输入数量的格式。

use std::io;

fn main() {
    println!("欢迎来到水果市场!");
    println!("请选择要购买的水果。\n");

    let valid_inputs = ["苹果", "香蕉", "橘子", "芒果", "葡萄", "quit", "q"];

    'mart: loop {
        let mut user_input = String::new();
        let mut quantity = String::new();

        println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
        println!("购买完成后,请输入“quit”或“q”。\n");

        // 读取用户输入
        io::stdin()
            .read_line(&mut user_input)
            .expect("无法读取用户输入。");
        user_input = user_input.trim().to_lowercase();

        // 验证用户输入
        let mut input_error = true;
        for input in valid_inputs {
            if input == user_input {
                input_error = false;
                break;
            }
        }

        // 处理无效输入
        if input_error {
            println!("错误: 请输入有效的输入");
            continue 'mart;
        }

        // 如果用户想要退出,就退出
        if user_input == "q" || user_input == "quit" {
            break 'mart;
        }

        // 获取数量
        println!(
            "\n你选择购买的水果是 \"{}\"。请输入以千克为单位的数量。
(1 千克 500 克的数量应该输入为 '1.5'。)",
            user_input
        );
        io::stdin()
            .read_line(&mut quantity)
            .expect("无法读取用户输入。");
    }
}

在第 11 行,我声明了另一个可变变量,它的值是一个空字符串,在第 48 行,我接受了用户的输入,但是这次是用户打算购买的水果的数量。

解析数量

我刚刚增加了一些代码,以已知的格式接受数量,但是这些数据被存储为字符串。我需要从中提取出浮点数。幸运的是,这可以通过 parse() 方法来完成。

就像 read_line() 方法一样,parse() 方法返回一个 Result 枚举。parse() 方法返回 Result 枚举的原因可以通过我们试图实现的内容来轻松理解。

我正在接受用户的字符串,并尝试将其转换为浮点数。浮点数有两个可能的值。一个是浮点数本身,另一个是小数。

字符串可以包含字母,但是浮点数不行。所以,如果用户输入的不是浮点数和小数,parse() 函数将会返回一个错误。

因此,这个错误也需要处理。我们将使用 expect() 函数来处理这个错误。

use std::io;

fn main() {
    println!("欢迎来到水果市场!");
    println!("请选择要购买的水果。\n");

    let valid_inputs = ["苹果", "香蕉", "橘子", "芒果", "葡萄", "quit", "q"];

    'mart: loop {
        let mut user_input = String::new();
        let mut quantity = String::new();

        println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
        println!("购买完成后,请输入“quit”或“q”。\n");

        // 读取用户输入
        io::stdin()
            .read_line(&mut user_input)
            .expect("无法读取用户输入。");
        user_input = user_input.trim().to_lowercase();

        // 验证用户输入
        let mut input_error = true;
        for input in valid_inputs {
            if input == user_input {
                input_error = false;
                break;
            }
        }

        // 处理无效输入
        if input_error {
            println!("错误: 请输入有效的输入");
            continue 'mart;
        }

        // 如果用户想要退出,就退出
        if user_input == "q" || user_input == "quit" {
            break 'mart;
        }

        // 获取数量
        println!(
            "\n你选择购买的水果是 \"{}\"。请输入以千克为单位的数量。
(1 千克 500 克的数量应该输入为 '1.5'。)",
            user_input
        );
        io::stdin()
            .read_line(&mut quantity)
            .expect("无法读取用户输入。");

        let quantity: f64 = quantity
            .trim()
            .parse()
            .expect("请输入有效的数量。");

    }
}

如你所见,我通过变量遮蔽将解析后的浮点数存储在变量 quantity 中。为了告诉 parse() 函数,我的意图是将字符串解析为 f64,我手动将变量 quantity 的类型注释为 f64

现在,parse() 函数将会解析字符串并返回一个 f64 或者一个错误,expect() 函数将会处理这个错误。

计算价格 + 最后的修饰

现在我们知道了用户想要购买的水果及其数量,现在是时候进行计算了,并让用户知道结果/总价了。

为了真实起见,我将为每种水果设置两个价格。第一个价格是零售价,我们在购买少量水果时向水果供应商支付的价格。水果的第二个价格是当有人批量购买水果时支付的批发价。

批发价将会在订单数量大于被认为是批发购买的最低订单数量时确定。这个最低订单数量对于每种水果都是不同的。每种水果的价格都是每千克多少卢比。

想好了逻辑,下面是最终的程序。

use std::io;

const APPLE_RETAIL_PER_KG: f64 = 60.0;
const APPLE_WHOLESALE_PER_KG: f64 = 45.0;

const BANANA_RETAIL_PER_KG: f64 = 20.0;
const BANANA_WHOLESALE_PER_KG: f64 = 15.0;

const ORANGE_RETAIL_PER_KG: f64 = 100.0;
const ORANGE_WHOLESALE_PER_KG: f64 = 80.0;

const MANGO_RETAIL_PER_KG: f64 = 60.0;
const MANGO_WHOLESALE_PER_KG: f64 = 55.0;

const GRAPES_RETAIL_PER_KG: f64 = 120.0;
const GRAPES_WHOLESALE_PER_KG: f64 = 100.0;

fn main() {
    println!("欢迎来到水果市场!");
    println!("请选择要购买的水果。\n");

    let valid_inputs = ["苹果", "香蕉", "橘子", "芒果", "葡萄", "quit", "q"];

    'mart: loop {
        let mut user_input = String::new();
        let mut quantity = String::new();

        println!("\n可以购买的水果:苹果、香蕉、橘子、芒果、葡萄");
        println!("购买完成后,请输入“quit”或“q”。\n");

        // 读取用户输入
        io::stdin()
            .read_line(&mut user_input)
            .expect("无法读取用户输入。");
        user_input = user_input.trim().to_lowercase();

        // 验证用户输入
        let mut input_error = true;
        for input in valid_inputs {
            if input == user_input {
                input_error = false;
                break;
            }
        }

        // 处理无效输入
        if input_error {
            println!("错误: 请输入有效的输入");
            continue 'mart;
        }

        // 如果用户想要退出,就退出
        if user_input == "q" || user_input == "quit" {
            break 'mart;
        }

        // 获取数量
        println!(
            "\n你选择购买的水果是 \"{}\"。请输入以千克为单位的数量。
(1 千克 500 克的数量应该输入为 '1.5'。)",
            user_input
        );
        io::stdin()
            .read_line(&mut quantity)
            .expect("无法读取用户输入。");

        let quantity: f64 = quantity
            .trim()
            .parse()
            .expect("请输入有效的数量。");

        total += calc_price(quantity, user_input);
    }

    println!("\n\n总价是 {} 卢比。", total);
}

fn calc_price(quantity: f64, fruit: String) -> f64 {
    if fruit == "apple" {
        price_apple(quantity)
    } else if fruit == "banana" {
        price_banana(quantity)
    } else if fruit == "orange" {
        price_orange(quantity)
    } else if fruit == "mango" {
        price_mango(quantity)
    } else {
        price_grapes(quantity)
    }
}

fn price_apple(quantity: f64) -> f64 {
    if quantity > 7.0 {
        quantity * APPLE_WHOLESALE_PER_KG
    } else {
        quantity * APPLE_RETAIL_PER_KG
    }
}

fn price_banana(quantity: f64) -> f64 {
    if quantity > 4.0 {
        quantity * BANANA_WHOLESALE_PER_KG
    } else {
        quantity * BANANA_RETAIL_PER_KG
    }
}

fn price_orange(quantity: f64) -> f64 {
    if quantity > 3.5 {
        quantity * ORANGE_WHOLESALE_PER_KG
    } else {
        quantity * ORANGE_RETAIL_PER_KG
    }
}

fn price_mango(quantity: f64) -> f64 {
    if quantity > 5.0 {
        quantity * MANGO_WHOLESALE_PER_KG
    } else {
        quantity * MANGO_RETAIL_PER_KG
    }
}

fn price_grapes(quantity: f64) -> f64 {
    if quantity > 2.0 {
        quantity * GRAPES_WHOLESALE_PER_KG
    } else {
        quantity * GRAPES_RETAIL_PER_KG
    }
}

对比之前的版本,我做了一些改动……

水果的价格可能会波动,但是在我们程序的生命周期内,这些价格不会波动。所以我将每种水果的零售价和批发价存储在常量中。我将这些常量定义在 main() 函数之外(即全局常量),因为我不会在 main() 函数内计算每种水果的价格。这些常量被声明为 f64,因为它们将与 quantity 相乘,而 quantityf64。记住,Rust 没有隐式类型转换 ?

当水果名称和用户想要购买的数量被存下来之后,calc_price() 函数被调用来计算用户指定数量的水果的价格。这个函数接受水果名称和数量作为参数,并将价格作为 f64 返回。

当你看到 calc_price() 函数的内部时,你会发现它是许多人所说的包装函数。它被称为包装函数,因为它调用其他函数来完成它的脏活。

因为每种水果都有不同的最低订单数量,才能被认为是批发购买,为了确保代码在未来可以轻松维护,每种水果都有单独的函数负责计算价格。

所以,calc_price() 函数所做的就是确定用户选择了哪种水果,并调用相应的函数来计算所选水果的价格。这些水果特定的函数只接受一个参数:数量。这些水果特定的函数将价格作为 f64 返回。

现在,price_*() 函数只做一件事。它们检查订单数量是否大于被认为是批发购买的最低订单数量。如果是这样,quantity 将会乘以水果的每千克批发价格。否则,quantity 将会乘以水果的每千克零售价格。

由于乘法行末尾没有分号,所以函数返回乘积。

如果你仔细看看 calc_price() 函数中水果特定函数的函数调用,这些函数调用在末尾没有分号。这意味着,price_*() 函数返回的值将会被 calc_price() 函数返回给它的调用者。

而且 calc_price() 函数只有一个调用者。这个调用者在 mart 循环的末尾,这个调用者使用这个函数返回的值来增加 total 的值。

最终,当 mart 循环结束(当用户输入 qquit 时),存储在变量 total 中的值将会被打印到屏幕上,并且用户将会被告知他/她需要支付的价格。

总结

这篇文章中,我使用了之前讲解的 Rust 编程语言的所有主题来创建一个简单的程序,这个程序仍然在某种程度上展示了一个现实世界的问题。

现在,我写的代码肯定可以用一种更符合编程习惯的方式来写,这种方式最好地使用了 Rust 的喜爱特性,但是我还没有讲到它们!

所以,敬请关注后续的 将 Rust 带入下一个层次 系列,并学习更多 Rust 编程语言的内容!

Rust 基础系列到此结束。欢迎你的反馈。

(题图:MJ/6d486f23-e6fe-4bef-a28d-df067ef2ec06)


via: https://itsfoss.com/milestone-rust-program/

作者:Pratham Patel 选题:lkxed 译者:Cubik65536 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在 Rust 系列的 上一篇文章 中,我介绍了如何使用 ifelse 关键字来处理 Rust 程序的控制流。

这是处理程序控制流的一种方法。另一种方法是使用循环。因此,让我们在本文中看看循环。

Rust 中可用的循环

Rust 编程语言有三种不同的循环,基于你想要实现什么以及可用的内容:

  • for
  • while
  • loop

我假设你对 forwhile 已经很熟悉了,但 loop 对你来说可能是个新概念。让我们先从熟悉的概念开始。

for 循环

for 循环主要用于迭代一种称为迭代器的东西。

这个迭代器可以从任何东西中创建,从数组、向量(很快就会介绍!)、一系列值,或者任何自定义的东西。这里的可能性是无限的。

来看看 for 循环的语法。

for 迭代变量 in 迭代器 {
    <语句>;
}

其中的 迭代变量 在大多数其他编程语言教程中通常被称为 i ; )

迭代器 可以是任何东西,只要它能告诉下一个值是什么,如果有的话。

来通过一个程序来理解这个。

fn main() {
    let my_arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

    println!("迭代数组");
    for element in my_arr {
        println!("{}", element);
    }

    println!("\n迭代一个真正的迭代器");
    for element in my_arr.iter() {
        println!("{}", element);
    }

    println!("\nPython 风格的范围");
    for element in 0..10 {
        println!("{}", element);
    }
}

这里,我声明了一个数组,它包含从 0 到 9 的 10 个数字。在第 5 行的 for 循环中,我只是将这个数组指定为迭代器,Rust 会自动处理对这个数组的所有元素的迭代。不需要花哨的 my_arr[i] 魔法。

但是,在第 10 行,我调用了 .iter() 函数。这是一个明确的提及,它基于 my_arr 的值来获取一个迭代器。这个循环和第 5 行的循环之间唯一的区别是,这里你是通过在数组上调用 .iter() 函数来明确地调用它的。

在这个上下文环境中,在一个数据类型上调用 .iter() 函数不是必须的。因为这是一个数组,是语言本身提供的一种数据类型,Rust 已经知道如何处理它了。但是你 需要 在自定义数据类型中使用它。

最后,在第 15 行,我们有一个循环,它循环遍历一个范围。嗯,差不多是这样。如果你仔细看,这个范围看起来很像切片 “类型”。Rust 也知道这一点,并且 你处理了迭代(哈哈,明白了吗?)。

LCTT 译注:此处的梗是,“为你处理了迭代” 的英文原文是 “handles iteration for you",其中的 “for” 与 “for 循环” 的 “for” 是同一个单词。

输出如下:

迭代数组
0
1
2
3
4
5
6
7
8
9

迭代一个真正的迭代器
0
1
2
3
4
5
6
7
8
9

Python 风格的范围
0
1
2
3
4
5
6
7
8
9

while 循环

while 循环可以被认为是非常类似于 if 条件语句。使用 if 语句,只要用户提供的条件为 trueif 语句体中的代码就会被执行 一次

但是,在 while 循环中,如果条件评估为 true,循环就会开始循环循环体。只要条件继续评估为 true,循环就会继续迭代。

while 循环只有在循环完成当前迭代中所有语句的执行并且在检查条件时,它的结果为 false 时才会停止。

来看看 while 循环的语法...

while 条件 {
    <语句>;
}

看到了吗?和 if 条件语句非常相似!不过没有 else 块 ; )

来看一个程序来更好地理解这个。

fn main() {
    let mut var = 0;

    while var < 3 {
        println!("{var}");
        var += 1;
    }
}

我有一个可变变量 var,它的初始值为 0。只要可变变量 var 中存储的值小于 3,while 循环就会执行。

在循环中,var 的值被打印出来,然后它的值被增加 1。

这是上面代码的输出:

0
1
2

loop 循环

Rust 有一个无限循环。是的,一个没有开始条件和停止条件的循环。它只是一直循环,直到永远。当然,它有触发器来停止代码本身的循环执行。

无限循环的语法如下:

loop {
    <语句>;
}
? 这些循环主要用于 GUI 软件,退出是一个 显式 操作。

在我给你一个例子之前,因为这个循环非常特殊,让我们先看看如何 退出 它 :p

要停止无限循环的执行,需要在循环内使用 break 关键字。

来看一个例子,只有 0 到 3 之间的整数(包括 0 和 3)才会被打印到程序输出。

fn main() {
    let mut var = 0;

    loop {
        if var > 3 {
            break;
        }

        println!("{}", var);
        var += 1;
    }
}

看待这个特定的例子的最好方法是将它看作是一个增加了一堆没有必要的东西的 while 循环 ; )

你有一个可变变量 var,它的初始值为 0,它被用作迭代器。无限循环从一个 if 条件开始,如果 var 的值大于 3,break 关键字就会被执行。后来,就像 while 循环的前一个例子一样,var 的值被打印到标准输出,然后它的值被增加 1。

它的输出如下:

0
1
2
3

标记循环

假设有两个无限循环,一个嵌套在另一个中。由于某种原因,退出条件在最内层循环中被检查,但这个退出条件是为了退出最外层循环。

在这种情况下,标记循环可能是有益的。

? breakcontinue 关键字并不仅仅用于无限循环。它们可以用于 Rust 语言提供的所有三种循环。

接下来是如何标记循环。

'标记: loop {}

要告诉编译器一个循环被标记了,从一个单引号字符开始,输入它的标签,然后跟着一个冒号。然后,继续使用你通常定义循环的方式。

当你需要退出某个循环时,只需像这样指定循环标签:

break '标记;

来看一个例子来更好地理解这个。

fn main() {
    let mut a = 0;
    let mut b = 0;

    'parent: loop {
        a += 1;

        loop {
            println!("a: {}, b: {}", a, b);
            b += 1;

            if a + b == 10 {
                println!("\n{} + {} = 10", a, b);
                break 'parent;
            }
        }
    }
}

这里,我使用两个可变变量 ab,它们的初始值都设置为 0。

然后,最外层的循环被标记为 parentparent 循环将变量 a 的值增加 1,并有一个内部/子循环。

这个(在第 8 行的)子循环打印变量 ab 的值。在这个循环内部,变量 b 的值增加了 1。退出条件是 a + b == 10。这意味着只要变量 ab 中存储的值相加,结果为 10,parent 循环就会被打破。即使第 14 行的 break 条件“属于”内部循环,它也会打破 parent 循环。

来看看程序的输出。

a: 1, b: 0
a: 1, b: 1
a: 1, b: 2
a: 1, b: 3
a: 1, b: 4
a: 1, b: 5
a: 1, b: 6
a: 1, b: 7
a: 1, b: 8

1 + 9 = 10

就像从程序输出中可以看出的那样,循环在 ab 分别具有值 1 和 9 时停止。

continue 关键字

如果你已经在其他编程语言(如 C/C++/Java/Python)中使用过循环,你可能已经知道 continue 关键字的用法。

break 关键字用于完全停止循环执行时,continue 关键字用于“跳过”循环执行的 当前迭代 并从下一迭代开始(如果条件允许)。

来看一个例子来理解 continue 关键字的工作原理。

fn main() {
    for i in 0..10 {
        if i % 2 == 0 {
            continue;
        }
        println!("{}", i)
    }
}

在上面的代码中,我有一个 for 循环,它迭代了 0 到 9 之间的整数(包括 0 和 9)。一旦循环开始,我就设置了一个条件检查,看看这个数字是不是偶数。如果这个数字是偶数,continue 关键字就会被执行。

但是如果这个数字是奇数,这个数字就会被打印到程序输出。

来看看这个程序的输出。

1
3
5
7
9

正如你所看到的,循环似乎一直在“进行”,尽管 0 到 9 之间显然有偶数。但是因为我使用了 continue 关键字,当遇到这个关键字时,循环执行就会停止。

这个循环跳过了它下面的任何东西,并继续下一次迭代。这就是为什么偶数没有被打印出来,但是 0 到 9 之间的所有奇数都被打印到了程序输出中。

总结

要总结这篇长文,我演示了 3 种不同循环的用法:forwhileloop。我还讨论了两个关键字,它们影响这些循环的控制流:breakcontinue

我希望你现在能理解每个循环的适当用例。如果你有任何问题,请告诉我。

(题图:MJ/25579e09-ae1c-47d3-8266-3bd9a54456c0)


via: https://itsfoss.com/rust-loops/

作者:Pratham Patel 选题:lkxed 译者:Cubik65536 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出