从零编写-Linux-操作系统笔记-全-

从零编写 Linux 操作系统笔记(全)

001:概述

在本节课中,我们将一起学习如何从零开始构建一个完整的Linux操作系统。我们将了解整个构建过程的概览、背后的原理以及需要经历的各个阶段。

计算机启动过程

为了理解如何构建一个可启动的系统,我们首先需要了解计算机的启动过程。上一节我们介绍了课程目标,本节中我们来看看计算机是如何启动的。

你的计算机主板上包含以下核心部件:

  • CPU
  • 硬盘(或SSD等存储设备)
  • 随机存取存储器(RAM)
  • BIOS(存储在ROM或EEPROM芯片中的固件)

当你启动计算机时,会发生以下一系列事件:

  1. 主板首先运行存储在BIOS中的固件。
  2. 固件被复制到RAM中,CPU的指令指针被设置为开始执行这段代码。
  3. 固件指示CPU与硬盘通信,并将硬盘的第一个扇区(即主引导记录,MBR)复制到RAM中。
  4. 固件随后指示CPU开始执行MBR中的代码。MBR中包含一个引导加载程序(如GRUB)。
  5. 引导加载程序了解分区表和文件系统。它会根据其配置文件,找到并加载位于例如 /boot/ 目录下的内核文件(如 vmlinuz)到RAM中。
  6. 引导加载程序设置CPU的指令指针,开始执行内核代码。

Linux系统的启动

在之前的步骤中,我们加载了内核。然而,内核的主要职责是管理进程间的通信(包括驱动程序、系统服务等),而不是直接运行所有程序。将过多功能放在内核中会带来安全风险。

在现实中,内核启动后,会运行一个初始化系统(如传统的 init 或现代的 systemd)。这个初始化系统负责后续的所有启动任务,例如:

  • 根据配置文件(如 /etc/fstab)挂载文件系统(例如将根分区 / 和启动分区 /boot 挂载到正确位置)。
  • 启动各种系统服务和守护进程(如网络驱动、DHCP客户端等)。

因此,一个可启动的Linux系统需要包含:主引导记录(MBR)、引导分区(包含内核)、根分区(包含所有程序、库、配置文件和初始化系统)

构建我们的Linux系统

现在我们已经了解了启动原理,本节中我们来看看如何实际构建这样一个系统。我们将使用一个USB闪存驱动器作为我们的“硬盘”。

我们需要在这个USB驱动器上创建以下结构:

  1. 主引导记录(MBR)
  2. 引导分区(约100MB,用于存放内核和引导加载程序)
  3. 根分区(剩余空间,用于存放完整的操作系统)

以下是构建过程的主要步骤:

  • 使用 fdisk 工具在USB驱动器上创建分区表。
  • 在分区上创建文件系统(如ext4)。
  • 编译并安装所有必要的程序、库和配置文件到根分区。
  • 编译Linux内核和GRUB引导加载程序,并将其安装到引导分区。
  • 使用GRUB将引导加载程序写入MBR。
  • 配置初始化系统(如 systemdsysvinit)以在启动时正确加载服务和驱动。

完成这些步骤后,我们就可以从这块USB驱动器启动我们自定义的Linux系统了。

Linux From Scratch (LFS) 构建阶段

Linux From Scratch 项目将整个构建过程系统地分为三个阶段,以确保构建环境的纯净和目标系统的独立性。

第一阶段:构建准备

在这个阶段,我们在现有的主机操作系统(称为构建机器 A)上工作。我们使用主机系统的包管理器安装必要的开发工具链(如 gcc, make, autoconf 等)。然后,我们下载所有后续阶段需要的源代码包。

第二阶段:交叉编译工具链

这是关键的一步。我们使用主机(A)的编译器,编译出一个新的、能在主机(A)上运行但能为中间系统 B 生成代码的交叉编译器。这个交叉编译器用于编译一系列基础工具(如 coreutils, bash 等),这些工具将构成临时系统 B 的基础环境。

第三阶段:隔离构建与目标系统

接下来,我们将USB驱动器的根分区挂载到主机的一个目录下(例如 /mnt/lfs),然后使用 chroot 命令切换到该目录。chroot 环境创造了一个高度隔离的视图,使得 /mnt/lfs 看起来就像是系统的根目录 /。这模拟了从目标系统启动后的环境。

chroot 环境中:

  1. 我们使用之前为系统 B 构建的交叉编译器,再次编译所有软件包。但这次,我们使用一个能为最终目标系统 C 生成代码的编译器(它本身运行在系统 B 上)。
  2. 我们编译出的所有程序都将被安装到USB驱动器的根分区中。
  3. 最后,我们还需要构建一个能在最终目标系统 C 上运行并为自身生成代码的“本地”编译器。

完成所有编译后,我们还需要编译Linux内核和GRUB,配置引导加载程序,并编写所有必要的系统配置文件(如网络、服务启动等)。

课程总结与展望

本节课中我们一起学习了从零构建Linux操作系统的核心概念和完整流程。我们回顾了计算机的启动过程,明确了构建一个可启动系统所需的组件(MBR、引导分区、根分区)。我们重点介绍了Linux From Scratch项目采用的严谨的三阶段构建方法,它通过构建临时系统、交叉编译和 chroot 环境隔离,确保了最终系统的纯净性和独立性。

整个过程中,大量的时间将花费在重复的 ./configure && make && make install 编译步骤上。在后续的实际操作视频中,我将利用这些编译时间,深入讲解Linux的目录结构(如 /bin, /lib, /etc 的作用)、各个核心软件包的功能以及系统配置的要点。我希望这不仅能带你完成一个系统的构建,更能让你深刻理解Linux系统的内部解剖结构。

如果你喜欢这个系列,请点赞、分享、订阅。我们下次再见。

003:下载软件包 📦

在本节课中,我们将学习如何下载构建LFS系统所需的所有软件包。上一节我们完成了USB存储设备的准备工作,包括分区、格式化和创建目录。本节中,我们将利用sources目录来下载所有必要的软件包,为后续的安装步骤做好准备。

准备工作

首先,我们需要获取软件包的下载信息。LFS手册的第3.2章提供了一个完整的软件包列表。我们将以此为基础,创建一个便于管理的列表文件。

以下是创建和管理软件包列表的步骤:

  1. 创建CSV文件:我们创建一个名为packages.csv的文件。CSV(逗号分隔值)是一种简单的表格格式,每行代表一条记录,列之间用分号分隔。
  2. 定义列结构:我们将列表整理为四列:
    • 第一列:软件包名称(全部转换为小写)。
    • 第二列:软件包版本号。
    • 第三列:下载URL。为了便于后续脚本处理,我们将URL中的版本号替换为@符号。
    • 第四列:MD5校验和。由于HTTP协议不安全,校验文件完整性是防止中间人攻击的良好实践。
  3. 转移工作环境:为了直接在USB设备上工作,我们将脚本和packages.csv文件复制到USB挂载点的LFS/sources目录下,并切换到此目录。

编写下载脚本

接下来,我们编写一个Bash脚本来自动化下载过程。脚本的核心逻辑是读取packages.csv文件,并逐行处理每个软件包。

脚本的主要流程如下:

#!/bin/bash
# 设置PATH环境变量,确保后续能使用交叉编译器等工具
export PATH=/tools/bin:$PATH

# 读取 packages.csv 文件
while IFS=';' read -r name version url md5sum; do
    # 处理URL:将占位符@替换为实际的版本号
    download_url=$(echo "$url" | sed "s/@/$version/g")
    # 从URL中提取文件名
    filename=$(basename "$download_url")

    # 检查文件是否已存在且有效
    if [ ! -f "$filename" ]; then
        echo "下载 $name-$version..."
        # 使用wget下载文件
        wget "$download_url"

        # 验证MD5校验和
        if ! echo "$md5sum  $filename" | md5sum -c --quiet > /dev/null 2>&1; then
            echo "错误:$filename 的MD5校验和不匹配!可能文件损坏或遭到篡改。"
            rm -f "$filename" # 删除无效文件
            exit 1
        fi
        echo "$name-$version 下载并验证成功。"
    else
        echo "$name-$version 已存在,跳过下载。"
    fi
done < packages.csv

代码解释

  • while read ... done < packages.csv:循环读取CSV文件的每一行。
  • IFS=';':将字段分隔符设置为分号,以正确解析CSV列。
  • sed "s/@/$version/g":使用流编辑器sed将URL中的@占位符全局替换为具体的版本号。
  • basename:从完整的URL路径中提取出文件名。
  • [ ! -f "$filename" ]:检查目标文件是否不存在,避免重复下载。
  • wget:用于从网络下载文件的命令。
  • md5sum -c:检查文件的MD5校验和是否与提供的值匹配。如果不匹配,则删除文件并报错。

运行与调试

运行脚本后,它会开始下载所有列出的软件包。这个过程可能需要一些时间。

在首次运行中,我们可能会遇到一些问题。例如,某些软件包名称(如P11-KitPython)在CSV文件中被错误地转换成了全小写,导致下载URL拼写错误。我们需要返回packages.csv文件,将这些名称的首字母修正为大写,然后重新运行脚本。

修正后,脚本应能成功下载所有软件包。最终,sources目录下应包含所有所需的源码包文件。

总结

本节课中我们一起学习了如何为LFS系统准备软件包。我们首先根据官方手册创建了一个结构化的软件包列表文件,然后编写了一个自动化脚本,该脚本能够读取列表、下载文件并验证其完整性。通过跳过已存在的有效文件,脚本还优化了下载过程。现在,所有必要的源码包都已准备就绪,下一节课我们将开始安装这些软件包,正式进入构建系统的核心阶段。

004:编译临时工具链

概述

在本节课中,我们将开始实际编译和安装构建LFS系统所需的核心工具链。我们将从第5章开始,安装binutilsGCCLinux API头文件glibclibstdc++等基础包。我们将创建一个自动化脚本来处理这些包的安装过程,并理解每个包在工具链中的作用。

上一节我们完成了所有源代码的下载和准备工作,本节中我们来看看如何开始编译这些基础工具。

创建章节目录

首先,我们需要为LFS手册的每个章节创建对应的目录,以便更好地组织我们的工作。

以下是创建目录的步骤:

mkdir chapter5 chapter6 chapter7 chapter8

理解binutils包

binutils包包含了一系列二进制工具,例如链接器(ld)、汇编器(as)以及其他用于处理目标文件和库的工具。

以下是binutils包中的一些核心工具:

  • ar:用于创建静态库归档文件。
  • ld:GNU链接器,用于将目标文件链接成可执行文件或共享库。
  • as:GNU汇编器。
  • nm:列出目标文件中的符号。
  • readelf:显示ELF格式目标文件的信息。

编写自动化安装脚本

为了避免手动输入冗长的编译命令,我们将创建一个名为package_install.sh的脚本。这个脚本将根据我们提供的包名和章节号,自动执行解压、配置、编译和安装过程。

脚本的核心逻辑如下:

  1. 接收两个参数:章节号和包名。
  2. 从我们之前创建的packages.csv文件中查找对应的包信息。
  3. 将源代码包解压到指定目录。
  4. 进入源代码目录,并执行该章节对应的安装脚本。
  5. 将整个安装过程的输出记录到日志文件中。

以下是脚本处理压缩包和目录的关键代码片段:

# 从文件名中提取目录名(去除.tar.*后缀)
dir_name=$(echo $cache_file | sed -E 's/(.*)\.tar\..*/\1/')
mkdir -p $dir_name
tar -xf $cache_file -C $dir_name

# 进入目录并保存当前路径
pushd $dir_name

# 如果解压后只有一个子目录,将其内容上移一层
if [ $(ls -1A | wc -l) -eq 1 ]; then
    sub_dir=$(ls -1A)
    mv $sub_dir/* .
    rmdir $sub_dir
fi

安装第5章的基础包

现在,我们可以使用脚本来安装第5章列出的所有临时工具。

以下是第5章需要安装的软件包列表及其简要说明:

  • binutils-2.42:第一遍编译,提供基础的二进制工具。
  • gcc-13.2.0:第一遍编译,GNU编译器集合。
  • linux-6.7.4:仅安装内核头文件,为glibc提供与内核通信的接口定义。
  • glibc-2.39:GNU C库,几乎所有程序都将链接到它,它是用户程序与Linux内核之间的桥梁。
  • libstdc++-13.2.0:GNU C++标准库,从GCC包中构建,为C++程序提供支持。

在运行安装脚本时,我们需要注意LFS手册中提供的配置参数。例如,在首次编译binutils时,手册建议禁用国际化(--disable-nls)并将警告视为错误(--disable-werror),以确保临时工具的纯净和构建过程的顺利。

处理GCC的依赖库

GCC编译依赖于多个数学运算库,如MPFRGMPMPC。LFS手册提供了特定的命令来解压这些库并将其移动到GCC源代码树中预期的目录名下。

为了避免硬编码版本号,我们可以使用tar命令的--strip-components=1选项来直接解压到目标目录。

相关命令如下:

tar -xf ../mpfr-4.2.1.tar.xz --strip-components=1 -C mpfr
tar -xf ../gmp-6.3.0.tar.xz --strip-components=1 -C gmp
tar -xf ../mpc-1.3.1.tar.gz --strip-components=1 -C mpc

编译过程与验证

运行安装脚本后,每个包的configuremakemake install步骤将自动执行。GCCbinutils的编译会花费较长时间。我们需要观察终端输出或日志文件,确保没有出现致命的错误信息。

编译成功的输出通常以大量的构建信息结束,并返回到命令提示符。

总结

本节课中我们一起学习了LFS构建过程中至关重要的一步——编译临时工具链。我们创建了自动化安装脚本,并成功安装了第5章的所有基础包,包括binutilsGCCLinux API头文件glibclibstdc++。这些工具为我们后续在chroot环境中构建其余的系统组件奠定了基础。

下一节,我们将进入第6章,并首次切换到chroot环境,开始构建真正属于我们LFS系统的工具链。

005:chroot环境搭建 🛠️

在本节课中,我们将继续构建LFS系统,重点完成第6章核心工具的编译,并最终进入关键的chroot环境。这是从“宿主系统”过渡到“目标系统”的重要一步。

上一节我们完成了第5章临时工具的编译。本节中,我们将编译第6章的工具链,并学习如何设置和进入chroot环境。

编译第6章工具链

第6章需要编译一系列核心工具,这些工具是后续构建目标系统的基础。以下是需要编译的软件包列表:

  • M4
  • Ncurses
  • Bash
  • Coreutils
  • Diffutils
  • File
  • Findutils
  • Gawk
  • Grep
  • Gzip
  • Make
  • Patch
  • Sed
  • Tar
  • Xz
  • Binutils (再次编译)
  • GCC (再次编译)

我们使用脚本来批量处理这些包的编译。脚本的核心逻辑是检查并清理旧的源码目录,然后执行标准的配置、编译和安装流程。例如,对于M4包,其安装脚本结构如下:

#!/bin/bash
# 设置版本等变量
# 检查并删除旧的源码目录
# 解压源码
# 进入源码目录并执行配置、编译、安装
./configure --prefix=/usr   \
            --host=$LFS_TGT \
            --build=$(build-aux/config.guess)
make
make DESTDIR=$LFS install

在编译M4时,我们遇到了一个关于过时编译器参数-Wabi的错误。解决方法是修改编译标志,在配置或编译步骤中传递正确的参数。我们通过设置CFLAGS环境变量来实现:

# 在配置或编译前设置
export CFLAGS="-Wno-error -fpermissive"

应用此修复后,M4及其他工具包得以成功编译。

备份工作成果

在进入chroot之前,强烈建议对已构建的系统进行备份。这可以防止后续操作失误导致前功尽弃。可以使用tar命令创建备份:

cd $LFS
tar -cJpf /path/to/backup/lfs-temp-tools-$(date +%s).tar.xz .

准备chroot环境

chroot(change root)能将当前进程的根目录切换到指定位置(这里是$LFS),从而创建一个隔离的系统环境。在进入之前,需要进行一系列准备。

以下是准备步骤的脚本概要:

  1. 权限设置:将$LFS下除sources外的所有目录所有者改为root,确保环境安全。
    chown -R root:root $LFS/{usr,lib,var,etc,bin,sbin,tools}
    
  2. 创建虚拟文件系统目录:在$LFS下创建/dev/proc/sys/run等目录,这些是系统运行时必需的。
  3. 挂载宿主系统目录:使用--bind选项将宿主系统的/dev/dev/pts/proc/sys/run目录绑定到$LFS下的对应位置,以便在chroot环境中访问设备和控制台。
    mount --bind /dev $LFS/dev
    mount --bind /dev/pts $LFS/dev/pts
    mount -t proc proc $LFS/proc
    mount -t sysfs sysfs $LFS/sys
    mount -t tmpfs tmpfs $LFS/run
    
  4. 创建设备节点:使用mknod命令创建设备文件,例如控制台(/dev/console)和空设备(/dev/null)。
    mknod -m 600 $LFS/dev/console c 5 1
    mknod -m 666 $LFS/dev/null c 1 3
    
  5. 处理共享内存:检查宿主系统的共享内存位置(通常是/dev/shm),并在chroot环境中创建相应的目录或符号链接。

进入chroot环境

完成准备工作后,即可使用chroot命令进入隔离环境。为了拥有一个完整可用的shell环境,我们需要设置正确的环境变量。

以下是进入chroot的脚本命令:

chroot "$LFS" /usr/bin/env -i \
    HOME=/root \
    TERM="$TERM" \
    PS1='(lfs chroot) \u:\w\$ ' \
    PATH=/usr/bin:/usr/sbin \
    /bin/bash --login +h
  • /usr/bin/env -i:启动一个干净的环境。
  • HOME=/root:设置家目录。
  • TERM:保留终端类型。
  • PS1:设置shell提示符,这里我们将其设为(lfs chroot) \u:\w\$ 以便清晰识别当前环境。
  • PATH:设置命令搜索路径,指向我们新编译的工具。
  • /bin/bash --login +h:启动bash登录shell,并启用历史记录功能(+h)。

为了验证是否成功进入chroot环境,可以在内部执行一个简单的测试命令,例如创建一个文件:

echo “test” > /test.txt

退出chroot后,检查$LFS/test.txt文件是否存在且内容正确,即可确认chroot环境运行正常。

总结

本节课中我们一起学习了如何编译LFS第6章的核心工具链,解决了编译过程中遇到的参数错误。更重要的是,我们详细讲解了chroot环境的原理、准备步骤和进入方法,并成功进入了为目标系统准备的隔离构建环境。这是构建独立Linux系统的关键里程碑。下一节,我们将在chroot环境中继续构建更多工具,并最终开始构建最终的目标系统。

006:目录、用户与组管理 🗂️👥

在本节课中,我们将学习如何在LFS(Linux From Scratch)环境中创建标准的Linux目录结构,并设置基本的用户和用户组。这是构建一个功能完整、符合Unix文件系统层次标准(FHS)的操作系统的重要一步。

上一节我们成功进入了chroot环境。本节中,我们将在这个环境中开始工作。

Linux目录结构概述

首先,我们来了解Linux系统(以及类Unix系统)中常见的目录及其作用。理解这些目录的用途,有助于我们明白为何要创建它们。

以下是Linux系统中一些核心目录的简要说明:

  • /boot:存放引导加载程序(boot loader)、其配置文件以及内核(kernel)文件。初始内存盘(initrd)文件通常也在这里。
  • /etc:存放系统范围的配置文件,例如硬件配置、网络设置等。它不包含用户的个人配置文件。
  • /home:存放各个用户的个人目录。每个用户在此目录下有一个以其用户名命名的子目录,用于存放个人文件、配置和下载内容等。
  • /bin:存放基本的用户命令二进制文件(即可执行程序),相当于Windows中的.exe文件。这些是系统启动和运行所必需的程序。
  • /sbin:存放系统管理命令的二进制文件,例如磁盘分区(fdisk)、文件系统创建(mkfs)等工具。普通用户通常无权访问此目录。
  • /usr:一个非常重要的目录,包含大量共享的、只读的用户数据和应用程序。其下通常有/usr/bin/usr/lib/usr/include等子目录。/usr目录可以被挂载在独立的网络或磁盘分区上,便于集中管理。
  • /lib/lib64:存放系统库文件。与Windows不同,Linux程序倾向于共享使用相同的库文件,以节省空间并保持一致性。
  • /root:系统管理员(root用户)的个人目录。它不位于/home下,是为了确保在/home目录无法挂载时,管理员仍能登录系统。
  • /dev:一个由内核在运行时创建的虚拟目录,包含代表各种硬件设备(如硬盘、USB设备)的特殊文件。访问这些文件即访问对应的硬件。
  • /proc:另一个虚拟文件系统,提供内核和进程信息的接口。例如,pstop等命令的信息就来源于此。
  • /sys:提供与内核交互的接口,包含关于设备、驱动、总线等系统底层信息。
  • /mnt/media:传统的挂载点目录,用于临时挂载文件系统(如光盘、USB驱动器)。现代系统更常用/media来自动挂载可移动介质。
  • /tmp/var/tmp:存放临时文件。
  • /opt/var/srv:这些目录的用途在不同Linux发行版中可能略有差异。/opt常用于存放第三方大型软件,/var存放经常变化的文件(如日志、缓存),/srv存放服务(如Web、FTP)提供的数据。

创建标准目录结构

现在,我们开始在chroot环境中创建上述目录结构。由于目录数量较多,我们将使用脚本命令来批量创建。

请注意,在chroot环境中,挂载点$LFS现在被视为根目录(/),因此我们直接在根目录下创建即可。

以下是创建目录的命令序列:

mkdir -pv /{boot,etc,home,mnt,opt}
mkdir -pv /{media/{floppy,cdrom},sbin,srv,var}
install -dv -m 0750 /root
install -dv -m 1777 /tmp /var/tmp
mkdir -pv /usr/{,local/}{bin,include,lib,sbin,src}
mkdir -pv /usr/{,local/}share/{color,dict,doc,info,locale,man}
mkdir -pv /usr/{,local/}share/{misc,terminfo,zoneinfo}
mkdir -pv /usr/{,local/}share/man/man{1..8}
mkdir -pv /var/{log,mail,spool}
ln -sv /run /var/run
ln -sv /run/lock /var/lock

命令解释:

  • mkdir -pv:递归创建目录,并显示创建过程。
  • install -dv -m MODE:创建目录并直接设置权限模式。
  • ln -sv:创建符号链接(软链接)。这里是为了兼容性,将/run/run/lock链接到传统的/var/run/var/lock路径。

创建基础系统文件

目录创建完毕后,我们需要创建一些基础的系统文件。

首先,创建/etc/mtab文件。这是一个历史遗留的兼容性文件,现代系统通常使用/proc/self/mounts。我们创建一个指向/proc/self/mounts的符号链接。

ln -sv /proc/self/mounts /etc/mtab

接下来,创建/etc/hosts文件。这个文件用于本地主机名解析,可以看作是一个本地的DNS缓存。

cat > /etc/hosts << EOF
127.0.0.1  localhost $(hostname)
::1        localhost
EOF

文件内容解释:

  • 127.0.0.1 是本地回环地址(IPv4)。
  • ::1 是本地回环地址(IPv6)。
  • $(hostname) 会替换为当前构建主机的名称。请注意,如果最终系统与构建主机在同一网络运行,主机名不应冲突。

创建系统用户和用户组

一个完整的系统需要预定义一些基本的用户和用户组,供系统服务和程序使用。

以下是创建基础系统用户和组的命令。我们通过编辑/etc/passwd(用户数据库)和/etc/group(组数据库)文件来实现。

创建系统用户:

cat > /etc/passwd << "EOF"
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/dev/null:/usr/bin/false
daemon:x:6:6:Daemon User:/dev/null:/usr/bin/false
messagebus:x:27:27:D-Bus Message Daemon User:/dev/null:/usr/bin/false
nobody:x:99:99:Unprivileged User:/dev/null:/usr/bin/false
EOF

字段解释(以root行为例,由冒号分隔):

  1. root:用户名。
  2. x:密码占位符(实际密码存储在/etc/shadow)。
  3. 0:用户ID(UID)。
  4. 0:主组ID(GID)。
  5. root:用户描述信息。
  6. /root:用户家目录。
  7. /bin/bash:用户登录后默认使用的shell。

注意,bindaemon等系统用户的登录shell被设置为/usr/bin/false,这意味着任何尝试以此用户身份登录的操作都会立即失败,这是一种安全措施。

创建系统用户组:

cat > /etc/group << "EOF"
root:x:0:
bin:x:1:daemon
sys:x:2:
kmem:x:3:
tape:x:4:
tty:x:5:
daemon:x:6:
floppy:x:7:
disk:x:8:
lp:x:9:
dialout:x:10:
audio:x:11:
video:x:12:
utmp:x:13:
usb:x:14:
cdrom:x:15:
adm:x:16:
messagebus:x:27:
input:x:28:
mail:x:34:
kvm:x:61:
wheel:x:97:
nogroup:x:99:
users:x:999:
EOF

字段解释(以root行为例):

  1. root:组名。
  2. x:组密码占位符。
  3. 0:组ID(GID)。
  4. 最后一个冒号后可以列出属于该组的附加用户(主组关系在/etc/passwd中定义)。

创建测试用户

在后续的软件包测试阶段,我们需要一个非root的普通用户来执行测试,以确保软件在普通用户权限下也能正常工作。

这里有一个关键点:我们目前仍在宿主系统(System A)的内核上运行。如果我们随意指定一个UID创建用户,宿主系统内核可能不认识这个UID,导致权限控制出现问题。因此,一个常见的技巧是使用当前构建者(你)在宿主系统中的UID来创建这个测试用户。

原始LFS手册使用了一个通过ttyls -n来获取当前终端设备所属用户UID的复杂方法,但这在脚本中可能失效。更简单可靠的方法是直接使用宿主系统的环境变量$UID

假设你的宿主系统用户UID是1001,创建测试用户的命令如下:

cat >> /etc/passwd << EOF
tester:x:1001:1001:Tester User:/home/tester:/bin/bash
EOF

同时,为测试用户创建家目录并设置权限:

mkdir -pv /home/tester
chown -v 1001:1001 /home/tester

注意: 请将上述命令中的1001替换为你自己在宿主系统中的实际UID。你可以通过在宿主系统的终端中运行echo $UIDid -u命令来查询。


本节课中我们一起学习了Linux标准目录结构的意义,并在chroot环境中成功创建了这些目录、基础系统文件以及必要的系统用户、用户组和一个用于测试的普通用户。现在,我们的LFS系统骨架已经搭建完毕,为后续编译和安装软件包打下了坚实的基础。下一节,我们将继续编译之旅,开始构建最终的系统软件。

007:最终构建系统

概述

在本节课中,我们将继续构建LFS系统,完成第七章的收尾工作。我们将进入一个新的chroot环境,安装剩余的软件包,并处理在此过程中遇到的一些问题。


进入新的chroot环境

上一节我们完成了基础系统的构建,并创建了必要的用户和组。本节中,我们将按照LFS手册的建议,进入一个新的chroot环境来继续安装软件包。

LFS手册建议在初始的chroot环境中再启动一个bash shell。这是因为第一个chroot环境中的bash以root身份运行,但此时/etc/passwd文件可能尚未完全就绪,导致用户ID没有对应的用户信息。启动第二个bash可以避免由此产生的问题。

虽然我们不确定为何不直接在初始脚本中完成这些操作,但我们将遵循手册的指导。我们的做法是:退出当前的chroot环境,然后通过一个循环脚本再次进入。

以下是创建和运行新chroot环境脚本的步骤:

# 创建 chroot2.sh 脚本
cat > chroot2.sh << "EOF"
#!/bin/bash
set -e

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/linux-os-scr/img/77264a30aa8a858ec63338e684f04aba_4.png)

# 设置LFS变量为空,因为我们在chroot环境中,路径已经是根“/”
export LFS=""

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/linux-os-scr/img/77264a30aa8a858ec63338e684f04aba_6.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/linux-os-scr/img/77264a30aa8a858ec63338e684f04aba_8.png)

# 进入源代码目录
cd /sources

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/linux-os-scr/img/77264a30aa8a858ec63338e684f04aba_10.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/linux-os-scr/img/77264a30aa8a858ec63338e684f04aba_12.png)

# 准备chroot环境并运行编译脚本
./prepare_chroot.sh
./package_install_check_7.sh

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/linux-os-scr/img/77264a30aa8a858ec63338e684f04aba_14.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/linux-os-scr/img/77264a30aa8a858ec63338e684f04aba_16.png)

EOF

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/linux-os-scr/img/77264a30aa8a858ec63338e684f04aba_18.png)

# 为脚本添加执行权限
chmod +x chroot2.sh


安装第七章的软件包

在新的chroot环境中,我们需要安装一系列新的软件包。以下是需要安装的软件包列表:

  • libstdc++
  • gettext
  • bison
  • perl
  • python
  • texinfo
  • util-linux

我们创建了一个名为package_install_check_7.sh的脚本来自动化安装过程。该脚本会为每个软件包创建目录、解压源码、编译并安装。


处理构建过程中的问题

在运行安装脚本时,我们遇到了一些错误,并逐一解决了它们。

问题一:找不到XZ软件包

脚本报错提示找不到xz文件。检查发现,问题在于软件包列表中的名称与实际下载的压缩包名称不匹配。我们修正了软件包名称,确保其与压缩包文件名一致。

问题二:文件移动遗漏隐藏文件

在解压某些软件包(如perl)时,其源码目录内包含以点.开头的隐藏文件。我们原有的移动命令mv * ../不会移动这些隐藏文件,导致编译失败。

解决方案:修改移动命令,显式包含隐藏文件。

# 修改前
mv * ../

# 修改后
mv .* * ../

问题三:链接器找不到库文件

在编译util-linux时,出现错误cannot find -lncursesw。这表明链接器在寻找名为libncursesw.so的共享库文件时失败。

检查发现,库文件实际存在于/usr/lib目录,但链接器可能只在/lib目录中查找。

解决方案:在/lib目录中为所需的库文件创建一个硬链接。

# 进入lib目录
cd /lib
# 创建硬链接
ln /usr/lib/libncursesw.so.6 libncursesw.so

创建硬链接后,编译得以继续进行。


第七章收尾工作

所有软件包安装完成后,LFS手册建议进行一些清理工作以节省空间,例如删除临时工具的文档和调试符号。不过,考虑到节省的空间(约120MB)相对于整个系统(约5GB)来说比例很小,我们可以选择跳过此步骤。

最后,在结束工作前,我们需要卸载虚拟文件系统。

# 卸载虚拟文件系统
umount $LFS/dev/pts
umount $LFS/dev
umount $LFS/proc
umount $LFS/sys
umount $LFS/run

如果卸载失败,重启系统后再执行上述命令即可。


总结

本节课中,我们一起学习了如何进入新的chroot环境来完成LFS第七章的构建。我们安装了libstdc++gettext等多个核心软件包,并解决了构建过程中遇到的“找不到软件包”、“遗漏隐藏文件”和“库文件链接”等问题。最后,我们完成了第七章的收尾工作,为进入下一阶段的构建做好了准备。

008:构建目标系统 🛠️

在本节课中,我们将正式开始构建最终可用的Linux系统。上一节我们完成了所有辅助工具的准备工作,本节我们将进入chroot环境,手动编译并安装目标系统的核心软件包。

概述

我们已经完成了第7章,这意味着所有构建最终系统所需的预备工具都已就绪。现在,我们将进入chroot环境,开始构建最终将包含在可用系统中的软件。为了更深入地理解过程,我们将采用更手动的方式,而不是完全依赖自动化脚本。

进入Chroot环境

为了更手动地操作系统,我们首先需要创建一个能进入chroot环境的脚本。

以下是创建chroot环境并启动bash的脚本步骤:

  1. 准备环境:首先运行prepare_chroot脚本,挂载必要的虚拟文件系统。
  2. 启动交互式Shell:在准备好的chroot环境中启动一个交互式的bash会话。
  3. 控制会话:确保我们能控制这个新启动的bash会话。

对应的脚本核心命令如下:

# 准备chroot环境
./prepare_chroot $LFS
# 进入chroot并启动bash
chroot "$LFS" /usr/bin/env -i \
    HOME=/root \
    TERM="$TERM" \
    PS1='(lfs chroot) \u:\w\$ ' \
    PATH=/usr/bin:/usr/sbin \
    /bin/bash --login

退出与清理

操作完成后,我们需要一个脚本来正确地卸载虚拟文件系统并清理chroot环境。

以下是清理步骤,与准备步骤顺序相反:

  1. 卸载虚拟文件系统:按顺序卸载/dev/pts/dev
  2. 恢复目录所有权:将$LFS目录下的dev目录所有权恢复给原用户。
  3. 删除临时目录:删除在$LFS下创建的devprocsysrun等目录。

对应的脚本核心命令如下:

# 卸载文件系统
umount $LFS/dev/pts
umount $LFS/dev
# 恢复所有权(假设在sudo环境下获取原用户和组)
chown -R $(id -un):$(id -gn) $LFS/dev
# 删除临时目录
rm -rf $LFS/{dev,proc,sys,run}

开始构建软件包

成功进入chroot环境后,我们就可以开始按照第8章的指南构建软件包了。我们将从一些基础包开始。

以下是前几个需要安装的软件包列表:

  • Man-pages:包含系统手册页。安装命令通常是简单的make install
  • Tcl:一种高级编程语言。使用make -j8命令可以利用多核处理器加速编译。
  • Expect:一个用于自动化交互式应用程序的工具。
  • DejaGNU:一个测试框架。
  • IANA-etc:提供网络协议和服务的数据文件,安装过程主要是复制文件。
  • Glibc:系统的C语言库,是核心组件之一。在安装时,我们需要配置系统的时区信息。

配置系统时区

在安装Glibc等包时,需要配置本地时区。例如,将时区设置为Europe/Berlin的命令如下:

ln -sf /usr/share/zoneinfo/Europe/Berlin /etc/localtime

总结与展望

本节课中,我们一起学习了如何手动进入chroot环境,并开始了目标系统软件包的构建。我们完成了前六个包的安装,包括Man-pages、Tcl、Expect等。第8章总共包含约81个软件包,构建整个系统是一个漫长的过程。

为了保持教程的效率和趣味性,在后续课程中,我们可能不会对每个包都进行如此详细的手动演示,但核心方法和原理是相通的。我们已经建立了进入和操作chroot环境的能力,这是构建LFS系统最关键的一步。

下一节课,我们将继续这个构建之旅。

009:讨论软件包与继续编译 🛠️

在本节课中,我们将继续构建最终的LFS系统,并深入了解正在安装的一系列核心软件包。上一节我们完成了Glibc等基础库的安装,本节中我们将批量编译更多工具,并探讨每个包的功能。

概述

我们将创建一系列安装脚本,并批量编译从 man-pagesvim 的数十个软件包。过程中,我们会解释每个包的作用,这对于理解一个最小Linux系统的构成至关重要。

创建批量安装脚本

为了避免重复劳动,我们将为所有待安装的软件包预先创建空的脚本文件。以下是需要创建脚本的软件包列表:

  • man-pages
  • tcl
  • expect
  • dejagnu
  • iana-etc
  • glibc
  • zlib
  • bzip2
  • xz
  • zstd
  • file
  • readline
  • m4
  • bc
  • flex
  • binutils
  • gmp
  • mpfr
  • mpc
  • attr
  • acl
  • libcap
  • shadow
  • gcc
  • pkg-config
  • ncurses
  • sed
  • psmisc
  • gettext
  • bison
  • grep
  • bash
  • libtool
  • gdbm
  • gperf
  • expat
  • inetutils
  • perl
  • xml::parser
  • intltool
  • autoconf
  • automake
  • kmod
  • libelf
  • libffi
  • openssl
  • python
  • ninja
  • meson
  • coreutils
  • check
  • diffutils
  • gawk
  • findutils
  • groff
  • grub
  • less
  • gzip
  • iproute2
  • kbd
  • libpipeline
  • make
  • patch
  • man-db
  • tar
  • texinfo
  • vim

zlib 的脚本已提前编写好。对于 m4 包,我们将采用之前视频中使用的解决方案来规避已知问题。

安装与编译过程

进入 chroot 环境后,我们开始执行第一批七个包的安装脚本:man-pages, tcl, expect, dejagnu, iana-etc, glibc, zlib

在编译进行的同时,让我们来讨论这些软件包的具体作用。

软件包功能详解

已安装的软件包

以下是第一批安装的软件包及其功能:

  • man-pages: 包含超过2200个程序、源代码和头文件的帮助文档。
  • tcl: 工具命令语言,一种高级脚本解释器。
  • expect: 用于自动化测试的程序,可以预设期望的输入输出。
  • dejagnu: 运行自动化测试的框架。
  • iana-etc: 提供 /etc/services/etc/protocols 文件,定义了TCP端口和协议的标准列表。
  • glibc: GNU C库,是系统最核心的库之一,处理本地化、日期时间格式化等基础功能。
  • zlib: 提供数据压缩功能的库。

正在编译的软件包

编译队列中的软件包同样关键:

  • bzip2, xz, zstd: 都是数据压缩工具和库。
  • file: 用于判断文件类型的工具。它不仅依赖扩展名,还会分析文件的魔数(magic number),并能检测文件编码。
    file script.sh # 输出: script.sh: Bourne-Again shell script, ASCII text executable
    
  • readline: 一个库,为命令行程序提供行编辑和历史记录功能(例如按上箭头查看历史命令)。
  • m4: 宏处理器,类似于C/C++预处理器,用于扩展宏定义。
  • bc: 一个任意精度计算器语言。
    echo “1 + 1” | bc # 输出: 2
    
  • flex: 词法分析器生成器。它根据规则将输入流(如源代码)转换为标记流,常与 bison 配合使用来构建编译器。
  • binutils: 包含二进制工具集,如链接器 ld、汇编器 as,以及分析目标文件的工具 readelfobjdump
  • gmp, mpfr, mpc: 高精度数学运算库,被GCC及其生成的程序广泛使用。
  • attr: 提供 setfattr 等工具,用于管理文件系统对象的扩展属性。
  • acl: 访问控制列表库,用于为不同用户设置精细的文件访问权限。
  • libcap: 将root超级权限划分为一系列独立权限的库,允许普通用户执行部分特权操作。
  • shadow: 用于管理用户、组和密码的套件,是登录系统的核心。
  • gcc: GNU编译器集合,包含C、C++等编译器,是Linux世界的基石。

继续安装更多工具

gcc 编译完成后,我们继续安装后续一批软件包,直到 bash

  • pkg-config: 在软件包配置和编译阶段,向构建工具传递头文件路径和库路径的工具。
  • ncurses: 用于控制文本终端颜色、光标定位的库,是许多文本界面程序的基础。
  • sed: 流编辑器,是功能最强大的Linux工具之一。它用于处理和转换文本流。
    echo “foo bar” | sed ‘s/o/a/g’ # 将所有的’o’替换为’a’,输出: faa bar
    
    sed 支持正则表达式和捕获组,分隔符不限于 /,可以使用任何字符。
    echo “foo bar” | sed ‘s:o:a:g’ # 使用冒号作为分隔符,输出: faa bar
    
  • psmisc: 进程管理工具集,包含 pstree(以树状显示进程)、killall 等。
  • gettext: 国际化与本地化框架,用于程序的翻译。
  • bison: 语法分析器生成器,与 flex 配合,用于构建编程语言的解析器。
  • grep: 强大的文本搜索工具,使用正则表达式过滤行。
    echo -e “foo\nbar\nbaz” | grep “ba.” # 匹配包含”ba”及后接一个字符的行,输出: bar, baz
    
    -v 参数可以反向选择,即输出不匹配的行。
  • bash: Bourne-Again Shell,是绝大多数Linux系统的默认命令解释器,是系统的基础。

最后一批核心工具

我们创建了剩余所有包的脚本并开始编译。以下是其中部分重要工具的介绍:

  • libtool: 封装了使用共享库的复杂性,提供一致、便携的接口。
  • gdbm: GNU数据库管理器,提供存储键值对、搜索和检索数据的原语。
  • gperf: 根据一组键生成完美哈希函数的工具,可用于优化字符串查找。
  • expat: 用于解析XML的C语言库。
  • inetutils: 基础网络工具集,如 ping, telnet, ftp, traceroute
  • perl: 实用提取和报告语言,一种高级脚本语言。
  • xml::parser: Perl的XML解析接口(基于expat)。
  • intltool: 用于自动化国际化工作的工具。
  • autoconf & automake: GNU自动构建工具,用于生成 configure 脚本和 Makefile
  • kmod: 用于管理内核模块的工具(如 modprobe, insmod)。
  • libelf: 用于处理ELF(可执行与可链接格式)文件的库。
  • libffi: 外部函数接口库,用于以可移植的方式调用不同约定的函数。
  • openssl: 实现SSL/TLS协议,用于加密网络连接的重要库。
  • python: 流行的解释型高级编程语言。
  • ninja: 一个注重速度的现代化构建系统(如Chrome项目使用)。
  • meson: 另一个构建系统,语法简洁。
  • coreutils: 包含最核心的Unix命令行工具,如 ls, cp, mkdir, cat, echo 等上百个命令。可以说,没有它,系统几乎无法操作。
  • check: C语言的单元测试框架。
  • diffutils: 包含 diffpatch 工具。diff 比较文件差异,patch 应用差异文件。这是版本控制系统(如Git)的核心机制。
  • gawk: GNU Awk,一种强大的文本处理和数据提取编程语言,语法类似C。
  • findutils: 包含 find 命令,用于在目录树中查找文件,功能极其强大。
    find . -name “*.sh” # 查找当前目录及子目录中所有.sh文件
    
  • groff: GNU troff文档格式化系统,用于排版和生成PDF等格式文档。
  • grub: GRand Unified Bootloader,我们将用它来引导最终的系统。
  • less: 一个功能丰富的文件查看器。
  • gzip: 经典的压缩工具。
  • iproute2: 高级网络配置工具集,用于管理路由表、策略路由等。
  • kbd: 键盘工具,用于加载键盘布局。
  • libpipeline: 用于以灵活方便的方式操作子进程管道的库。
  • make: 经典的自动化构建工具,通过定义目标和依赖关系来组织编译过程,功能非常强大。
  • patch: 用于将 diff 生成的补丁文件应用到原始文件上。
  • man-db: 包含查找和查看手册页的工具,如 man 命令。
  • tar: 经典的归档工具,用于打包和解包文件。
  • texinfo: 用于阅读、编写和转换Info格式文档的工具。
  • vim: 一款功能强大、高度可配置的文本编辑器,以其模态编辑(普通模式、插入模式等)著称。
    vim file.txt # 打开文件,默认为普通模式
    i # 进入插入模式,开始编辑
    <Esc> # 返回普通模式
    :wq # 保存并退出
    

总结

本节课中我们一起学习了如何批量创建安装脚本,并编译安装了LFS系统所需的大量核心软件包。我们详细探讨了从基础库(如glibc)、开发工具(如gcc、flex、bison)、系统工具(如coreutils、findutils)到编辑器(vim)等各类包的功能。这个过程虽然耗时(约10小时),但让我们深刻理解了一个可运行的Linux系统所依赖的众多组件。下一节,我们将进入初始化系统(System V init 或 systemd)的选择和配置阶段,并向最终可启动的系统迈进。

010:第9.5节-问题修正

在本节课中,我们将回顾并解决在构建Linux From Scratch过程中遇到的一些编译和测试问题。我们将分析日志文件,定位失败原因,并采取相应措施来修正脚本和跳过有问题的环节,以确保构建过程能够继续进行。


问题概述与日志分析

上一节我们完成了大部分工具的编译。然而,并非所有过程都如预期般顺利运行。

例如,tar工具的编译过程持续运行了很长时间,最终不得不通过按下 Control+C 来中断它。这预示着存在一些问题。

在检查日志文件后,我确认了问题的存在。当然,不可能阅读所有日志文件。以GCC的日志文件为例,它有近50万行,无法逐行阅读。

我通常的做法是直接查看日志文件的末尾,检查整个过程是否正常结束。在浏览这些日志时,有几个问题尤为突出。


具体问题与修正措施

以下是几个在自动测试环节出现问题的软件包及其处理方式。

1. Tar 工具测试问题

tar 的问题是它一直在运行,最终被我显式取消。其日志中提到“列出大于 2^33 字节的稀疏文件”。2^33 大约是 8GB。当我检查U盘时,发现 tar 工具的构建目录实际上有 10GB 之大,U盘几乎已满。

我不明白为何一个自动化测试需要生成 10GB 的示例数据。但无论如何,这让我们别无选择,只能禁用其自动测试。

修正方法:
在编译 tar 时,跳过 make check 步骤。

2. Gawk 工具测试问题

gawk 的测试以错误代码 2 结束。其日志文件相对较短,但同样出现在自动化测试环节。

修正方法:
同样,选择禁用 gawk 的自动测试。

3. Gnu A 工具测试问题

gnu-a 的测试也以错误结束,并且同样持续运行了很久。它请求一个伪终端,这可能在脚本运行时引发问题。

修正方法:
禁用 gnu-a 的自动测试。

4. Texinfo 工具测试问题

texinfo 启动了一些 make 进程并运行了几个自动测试。根据记忆,我也中断了这个过程。

修正方法:
禁用 texinfo 的自动测试。


缺失的软件包:IP Route 2

在检查日志文件数量时,我发现了一个不一致的地方。第8章有两个与这些脚本无关的章节。如果算上Vim,我们本应构建67个软件包,但日志文件显示只有66个。

经过核对,发现缺少 iproute2 软件包的日志文件。这意味着构建脚本根本没有尝试编译它。

问题根源:
检查 package.csv 文件,发现软件包名称为 iproute2,但对应的构建脚本名称却是 iproute(缺少了数字“2”)。

修正方法:
将构建脚本从 iproute 重命名为 iproute2,以确保它能被正确调用。

# 假设脚本位于 /mnt/lfs/sources 目录下
mv /mnt/lfs/sources/iproute /mnt/lfs/sources/iproute2

修正后,重新运行构建过程。虽然从中间环节重试可能存在风险(例如,后续软件的配置脚本可能依赖之前构建的组件),但为了不从头开始,我们决定尝试一下。


重新构建与备份

进入 chroot 环境后,我们重新构建了 texinfotariproute2gawkgdb 这几个有问题的软件包。

过程很快完成,没有出现明显的错误信息,这表明修正措施可能是有效的。

至此,我们已经编译了包括Vim在内的所有工具。我将在此处暂停,并创建一个系统备份。

暂停原因:
接下来的软件包是针对 System V init 系统的。我计划在此处分支视频系列:一个分支使用 System V init,另一个分支将使用 systemd。因为 Linux From Scratch 有两本手册,从此处开始它们将走向不同的方向。

备份完成后,我们将继续制作关于 System V init 系统的视频,后续再制作 systemd 版本。接下来的章节包含许多有趣的内容,敬请期待下一次的视频。


总结

本节课中,我们一起学习了如何分析和解决在构建 Linux From Scratch 时遇到的编译与测试问题。我们通过检查日志文件定位了 targawkgnu-atexinfo 的测试失败,并采取了禁用自动测试的解决方案。同时,我们发现了 iproute2 软件包因脚本名不匹配而缺失构建的问题,并通过重命名脚本进行了修正。最后,我们在一个关键节点创建了备份,为后续探索不同的初始化系统(System V init 与 systemd)做好了准备。

011:System V Init 与网络配置 🖥️

在本节课中,我们将完成第8章剩余的System V Init相关软件包的编译,并进入第9章,学习如何配置System V Init系统以及基本的网络设置。我们将重点理解/etc/inittab文件的结构和运行级别(runlevel)的概念。


章节 8 收尾:编译System V Init相关软件包

上一节我们完成了第8章的大部分内容。本节中,我们将编译最后几个专用于System V Init系统的软件包。

我们需要编译的软件包是 eudevprocpsutil-linux。编译完成后,我们将正式完成第8章。

以下是编译和安装这些软件包的命令序列。请注意,我们假设您已处于正确的构建环境中。

# 编译 eudev
tar -xf eudev-*.tar.gz
cd eudev-*/
./configure --prefix=/usr           \
            --bindir=/sbin          \
            --sbindir=/sbin         \
            --libdir=/usr/lib       \
            --sysconfdir=/etc       \
            --libexecdir=/lib       \
            --with-rootprefix=      \
            --with-rootlibdir=/lib  \
            --enable-manpages       \
            --disable-static
make
make install
cd ..

# 编译 procps
tar -xf procps-*.tar.gz
cd procps-*/
make
make install
cd ..

# 编译 util-linux
tar -xf util-linux-*.tar.gz
cd util-linux-*/
./configure ADJTIME_PATH=/var/lib/hwclock/adjtime   \
            --docdir=/usr/share/doc/util-linux-* \
            --disable-chfn-chsh  \
            --disable-login      \
            --disable-nologin    \
            --disable-su         \
            --disable-setpriv    \
            --disable-runuser    \
            --disable-pylibmount \
            --disable-static     \
            --without-python     \
            --without-systemd    \
            --without-systemdsystemunitdir
make
make install
cd ..

编译完成后,建议检查各软件包的日志文件(log files),确认没有严重的错误(Severe errors)。虽然可能会有一些警告(warnings),但只要没有错误,通常可以继续。


进入第9章:配置System V Init系统

现在,我们进入第9章。本章的核心是配置System V Init系统,理解其工作原理,特别是启动脚本(boot scripts)的作用。

首先,我们需要创建一个新的脚本,用于切换到最终的系统根目录并执行后续配置。我们将其命名为 init-change-root.sh

#!/bin/bash
# init-change-root.sh
chroot "$LFS" /tools/bin/env -i \
    HOME=/root                  \
    TERM="$TERM"                \
    PS1='(lfs chroot) \u:\w\$ ' \
    PATH=/bin:/usr/bin:/sbin:/usr/sbin \
    /tools/bin/bash --login +h

运行此脚本后,我们将进入最终的系统环境。


理解启动脚本与运行级别

System V Init 启动过程的核心是位于 /etc/rc.d/init.d/ 目录下的启动脚本。init 进程会根据运行级别(runlevel)来决定执行哪些脚本。

运行级别由 /etc/inittab 文件定义。以下是一个典型的 inittab 文件结构示例:

id:3:initdefault:
si::sysinit:/etc/rc.d/rc.sysinit
l0:0:wait:/etc/rc.d/rc 0
l1:1:wait:/etc/rc.d/rc 1
l2:2:wait:/etc/rc.d/rc 2
l3:3:wait:/etc/rc.d/rc 3
l4:4:wait:/etc/rc.d/rc 4
l5:5:wait:/etc/rc.d/rc 5
l6:6:wait:/etc/rc.d/rc 6
ca::ctrlaltdel:/sbin/shutdown -t3 -r now
1:2345:respawn:/sbin/agetty --noclear tty1 9600
2:2345:respawn:/sbin/agetty tty2 9600
3:2345:respawn:/sbin/agetty tty3 9600
4:2345:respawn:/sbin/agetty tty4 9600
5:2345:respawn:/sbin/agetty tty5 9600
6:2345:respawn:/sbin/agetty tty6 9600

让我们解读关键部分:

  • id:3:initdefault: 定义系统启动后默认进入的运行级别(这里是3,多用户文本模式)。
  • si::sysinit:/etc/rc.d/rc.sysinit 指定系统初始化脚本。
  • l3:3:wait:/etc/rc.d/rc 3 对于运行级别3,执行 /etc/rc.d/rc 3 脚本。wait 动作表示init会等待该脚本执行完毕。
  • 1:2345:respawn:/sbin/agetty ... 在运行级别2、3、4、5下,在tty1上启动一个getty登录进程。respawn 表示如果该进程终止,init会重新启动它。
  • ca::ctrlaltdel:/sbin/shutdown ... 定义按下Ctrl+Alt+Del组合键时的动作。

/etc/rc.d/rc 脚本会根据运行级别,进入 /etc/rc.d/rcN.d/ (N为运行级别) 目录。该目录中包含指向 /etc/rc.d/init.d/ 中实际脚本的符号链接。

  • S 开头的链接,会以 start 参数调用对应脚本,启动服务。
  • K 开头的链接,会以 stop 参数调用对应脚本,停止服务。

配置系统时钟与键盘

接下来,我们配置系统控制台。例如,设置键盘映射为德语布局并包含欧元符号:

# 设置控制台键盘映射
loadkeys de-latin1
# 设置控制台字体(可选)
setfont lat9w-16

创建 /etc/sysconfig/console 文件来持久化这些设置:

cat > /etc/sysconfig/console << "EOF"
KEYMAP="de-latin1"
FONT="lat9w-16"
EOF

配置Shell环境

我们创建 /etc/profile/etc/bashrc 文件,为所有用户设置统一的Shell环境变量。

以下是 /etc/profile 示例,设置本地化环境:

cat > /etc/profile << "EOF"
# /etc/profile
# 设置系统范围的全局环境变量

export LANG=de_DE.UTF-8
export INPUTRC=/etc/inputrc
EOF

创建 /etc/shells 文件,列出所有合法的用户登录shell:

cat > /etc/shells << "EOF"
# /etc/shells
/bin/sh
/bin/bash
EOF

这个文件是一个安全特性,防止用户意外地将无效程序(如 /bin/false)设置为自己的登录shell而导致无法登录。


配置网络

最后,我们配置基本的静态网络。创建 /etc/sysconfig/network-devices/ifconfig.eth0 文件:

cat > /etc/sysconfig/network-devices/ifconfig.eth0 << "EOF"
ONBOOT=yes
SERVICE=ipv4-static
IP=192.168.1.30
GATEWAY=192.168.1.1
PREFIX=24
BROADCAST=192.168.1.255
EOF
  • IP: 为本机设置的静态IP地址。
  • PREFIX=24: 子网前缀,等同于子网掩码 255.255.255.0。这定义了本地网络的范围。
  • GATEWAY: 网关地址,用于访问本子网外的网络。

然后配置DNS解析器,创建 /etc/resolv.conf 文件:

cat > /etc/resolv.conf << "EOF"
# /etc/resolv.conf
nameserver 192.168.1.1
EOF

设置主机名,并更新 /etc/hosts 文件,实现本地主机名解析:

echo "lfs" > /etc/hostname

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/linux-os-scr/img/575f8c16fd63c7fa2e9aed94f4bd5043_26.png)

cat > /etc/hosts << "EOF"
# /etc/hosts
127.0.0.1  localhost
192.168.1.30 lfs
EOF


总结

本节课中我们一起学习了:

  1. 完成了第8章,编译了 eudevprocpsutil-linux 软件包。
  2. 进入了第9章,深入了解了System V Init系统。
  3. 详细解析了 /etc/inittab 文件的构成和运行级别的概念,理解了 rc 脚本如何通过符号链接管理服务的启动与停止。
  4. 配置了系统控制台的键盘映射。
  5. 通过 /etc/profile/etc/bashrc 配置了系统范围的Shell环境。
  6. 完成了基本的静态网络配置,包括IP地址、网关、DNS和主机名。

现在,我们的系统已经具备了基本的启动和服务管理框架,并配置好了网络环境。下一章(第10章),我们将编译Linux内核、安装引导程序,并最终启动这个我们从零构建的系统。

012:内核编译与系统启动 🚀

在本节课中,我们将完成Linux From Scratch项目的最后一步:编译Linux内核并配置系统启动。我们将创建文件系统表、编译内核、安装引导程序,并最终尝试从我们构建的系统启动。

上一节我们完成了第9章的系统配置。本节中,我们来看看如何编译内核并使其能够启动。

挂载启动分区

首先,需要再次挂载USB密钥。这次,我们还需要挂载启动分区,因为需要将其配置为可启动状态。

创建文件系统表

以下是需要创建的文件系统表 /etc/fstab 的内容。它定义了系统启动时需要挂载的文件系统。

# 文件系统     挂载点       类型    选项               转储  检查顺序
/dev/sdb2      /            ext4    defaults            1     1
# /dev/sdb1    swap         swap    pri=1               0     0
/dev/sdb1      /boot        ext4    defaults            1     2
  • /dev/sdb2 是我们的根设备,将被挂载到根目录 /,文件系统类型是 ext4
  • 我们没有交换分区,因此注释掉相关行。
  • /dev/sdb1 是启动分区,将被挂载到 /boot

编译Linux内核

现在,我们进入最核心的步骤:编译Linux内核。关于内核编译的更多细节,可以参考专门的Linux内核编程系列视频。

以下是编译内核的基本步骤:

  1. 清理源代码目录:使用 make mrproper 命令清理目录,避免与之前可能失败的构建产生冲突。
  2. 生成默认配置:使用 make defconfig 命令生成一个默认的内核配置文件 .config。我们这里不进行手动配置。
  3. 编译内核:运行 make 命令开始编译内核。这个过程可能需要一些时间。
  4. 安装内核模块:使用 make modules_install 命令将编译好的内核模块安装到 /lib/modules 目录下。
  5. 复制内核映像:使用 make install 命令将内核映像、System.map文件和配置文件复制到 /boot 目录。

配置GRUB引导程序

内核编译完成后,需要配置GRUB引导程序,以便系统能够启动。

以下是配置GRUB的步骤:

  1. 安装GRUB:使用 grub-install 命令将GRUB安装到启动设备(例如 /dev/sdb)。注意,我们使用 --target=i386-pc 参数指定为传统BIOS模式,因为我们的USB密钥可能不支持UEFI。
  2. 生成GRUB配置文件:创建 /boot/grub/grub.cfg 文件。以下是其核心内容:
    # 加载ext2文件系统模块
    insmod ext2
    # 设置根设备为第一个硬盘
    set root=(hd0)
    # 设置内核路径,注意这里的路径是相对于GRUB的根设备
    linux /vmlinuz root=/dev/sdb2
    # 设置初始内存盘(如果有的话)
    # initrd /initrd.img
    boot
    
    • 需要指定正确的根设备(例如 root=/dev/sdb2)。
    • 如果从USB设备启动,内核可能来不及识别设备,可以尝试在内核参数中添加 rootwaitrootdelay=10

尝试启动与问题排查

配置完成后,尝试从构建的系统启动。

可能会遇到以下问题及解决方案:

  1. 无法从USB设备启动:确保在BIOS/UEFI设置中选择了正确的启动模式(传统/Legacy BIOS)。对于USB设备,有时必须使用UEFI模式,但如果GRUB未编译UEFI支持,则可能需要使用传统模式。
  2. 启动设备未找到:使用 fdisk 工具检查并确保启动分区已设置“可启动”标志。
    sudo fdisk /dev/sdb
    # 在fdisk交互界面中,使用 `a` 命令切换分区1的可启动标志,然后使用 `w` 命令写入。
    
  3. 内核恐慌:无法挂载根文件系统:这通常是因为内核启动速度太快,USB设备尚未就绪。在内核启动参数中添加 rootwaitrootdelay=10
    # 在GRUB命令行或grub.cfg的linux行中添加
    linux /vmlinuz root=/dev/sdb2 rootdelay=10
    

启动成功与后续步骤

成功解决上述问题后,系统将正常启动,出现登录提示符。登录后,可以执行基本命令,如 echoping 等,这标志着一个从零构建的Linux系统已经成功运行。

本节课中我们一起学习了如何编译Linux内核、配置GRUB引导程序以及解决启动过程中常见的各种问题,最终成功启动了我们的LFS系统。

超越LFS

Linux From Scratch 官网 上,有一个“超越Linux From Scratch”的章节。它提供了大量流行软件包的编译指南,例如SSH服务器、图形服务器(X Window System)、桌面环境等。你可以根据这些指南,继续为你的系统添加更多功能。

至此,从零构建Linux操作系统的主要旅程已经完成。如果你喜欢这个系列,欢迎分享和订阅。我们下次再见!

013:启动脚本详解 🚀

在本节课中,我们将深入剖析Linux系统的启动过程,特别是启动脚本(bootscripts)的工作原理。上一节我们完成了整个LFS系统的构建并成功启动,本节我们将详细拆解启动过程中各个脚本的作用,帮助你理解系统从开机到登录的完整流程。

概述

启动脚本是系统初始化过程的核心。它们负责在系统启动时设置硬件、挂载文件系统、加载内核模块、配置网络等关键任务。我们将逐一分析/etc/rc.d/rc3.d/目录下的脚本,了解每个脚本的功能和执行顺序。

启动流程回顾

计算机启动时,固件(Firmware)首先加载引导程序GRUB。GRUB读取其配置文件grub.cfg,找到内核文件(如vmlinuz)并加载它。内核启动后,会执行第一个用户空间程序/sbin/initinit程序读取/etc/inittab文件,根据默认运行级别(通常是3)执行相应的启动脚本。

启动脚本执行机制

/etc/rc.d/rc脚本是启动过程的总调度器。它的核心逻辑是遍历指定运行级别目录(如/etc/rc.d/rc3.d/)中的脚本,并按特定顺序执行。

以下是其执行逻辑的简化描述:

# 首先,停止所有以K开头的服务
for script in /etc/rc.d/rc3.d/K*; do
    $script stop
done

# 然后,启动所有以S开头的服务
for script in /etc/rc.d/rc3.d/S*; do
    $script start
done

脚本的执行顺序由文件名中的数字决定,数字越小,优先级越高。

启动脚本逐项解析

以下是/etc/rc.d/rc3.d/目录中主要脚本的详细说明。每个脚本通常接收startstop参数,并据此执行不同的操作。

1. S00mountfs

这是第一个执行的脚本。它的主要功能是挂载虚拟文件系统。

  • 作用:挂载procdevptstmpfssysfsshm等虚拟文件系统。这些是系统运行所必需的基础文件系统。
  • 对应操作:我们在“切换根环境”的视频中也手动执行过类似操作。

2. S05modules

此脚本负责加载内核模块。

  • 作用:读取/etc/sysconfig/modules文件中的模块列表,并使用modprobe命令逐一加载它们。
  • 关键命令modprobe <module_name>
  • 灵活性:你可以通过修改配置文件路径来改变模块列表的来源。

3. S10localhost

此脚本配置本地网络。

  • 作用
    1. 将IP地址127.0.0.1(localhost)分配给回环接口。
    2. /etc/hostname文件中读取并设置系统的主机名。

4. S20udev

此脚本启动设备管理守护进程udev

  • 作用
    1. 启动udev守护进程。
    2. udev为内核已发现的设备创建设备节点(位于/dev目录)。
    3. 后续如果有新的设备被内核发现(热插拔事件),udev会自动为其创建设备节点。

5. S30swap

此脚本管理交换空间(swap)。

  • 作用
    • start:运行swapon -a,启用所有在/etc/fstab中定义的交换分区。
    • stop:运行swapoff -a,禁用所有交换分区。
    • restart:先执行stop,再执行start

6. S40checkfs

此脚本执行文件系统检查。

  • 作用:运行fsck命令,检查并修复非根文件系统。根文件系统通常在启动早期由内核检查。

7. S45mountfs

此脚本挂载所有用户定义的文件系统。

  • 作用
    • start:运行mount -a,挂载/etc/fstab文件中定义的所有文件系统。
    • stop:运行umount -a,卸载所有文件系统。

8. S47cleanfs

此脚本执行清理任务。

  • 作用:清理临时目录(如/tmp),并检查一些系统目录的访问权限,确保系统环境整洁。

9. S50udev_retry

此脚本再次尝试设备发现。

  • 作用:由于在挂载根文件系统之前,某些设备的发现可能失败(例如,驱动模块位于尚未挂载的根文件系统上),此脚本在根文件系统挂载后,再次运行udev以发现这些设备。

10. S55console

此脚本配置控制台。

  • 作用
    1. 运行loadkeys,加载键盘映射。
    2. 运行setfont,设置控制台字体。
    • 这些也是我们在手动配置系统时执行过的步骤。

11. S60sysctl

此脚本应用内核参数调整。

  • 作用:运行sysctl -p /etc/sysctl.conf,加载并应用/etc/sysctl.conf文件中定义的内核参数。在我们的LFS系统中,此文件尚未创建。

12. S65clock

此脚本配置系统时钟。

  • 作用:运行hwclock命令,根据硬件时钟(RTC)设置或同步系统时间。有趣的是,在默认的LFS配置中,这个脚本似乎从未被执行,但它仍然存在以供需要时启用。

13. S70network

此脚本配置网络接口。

  • 作用:遍历/etc/sysconfig/network-devices/目录下的网络接口配置文件(如ifconfig.eth0),并对每个接口运行ifup命令来启动它。
  • 停止操作:运行stop时,它会运行ifdown并尝试卸载所有网络文件系统。需要注意的是,如果系统有多个网络接口,这可能会误卸载通过其他接口访问的网络资源。

特殊运行级别的脚本

有些脚本的工作方式比较特殊,它们主要在运行级别0(关机)和6(重启)中被调用。

S01halt 与 S06reboot

这两个脚本在正常启动(运行级别3)时被调用start参数不会执行任何操作。它们只在切换到运行级别0或6时,被rc脚本以stop参数调用。

  • S01halt:执行halt命令,关闭计算机。
  • S06reboot:执行reboot命令,重新启动计算机。

S05sendsignals

此脚本负责在关机或重启前终止所有进程。

  • 作用
    1. 首先向所有进程发送SIGTERM(信号15),请求它们优雅地终止。
    2. 等待一段时间。
    3. 向仍未退出的进程发送SIGKILL(信号9),强制终止它们。

总结

本节课中,我们一起深入学习了Linux系统的启动脚本。我们从GRUB加载内核开始,跟踪到init进程执行/etc/rc.d/rc脚本,并详细分析了运行级别3下每个启动脚本的具体功能,包括挂载文件系统、加载模块、配置网络、管理设备等。我们还了解了用于关机和重启的特殊脚本的工作机制。通过本次学习,你现在应该对Linux从按下电源键到出现登录提示符的整个初始化过程有了清晰而完整的认识。

014:第8章收尾与第9章系统配置 🛠️

在本节课中,我们将完成第8章剩余软件包的构建,并开始进入第9章的系统配置环节。我们将重点学习如何配置网络、设备管理、时钟、控制台以及系统服务等基础设置。


第8章收尾工作

上一节我们完成了第8章大部分软件包的构建。本节中,我们来处理最后几个软件包,以完成第8章的全部内容。

首先,我们需要创建并运行安装脚本。以下是需要执行的命令:

# 创建并运行安装脚本
./script_name.sh

在运行脚本前,需要检查软件包的下载位置。由于本教程最初基于System V版本的Linux From Scratch,最后几个软件包可能需要单独添加。检查后发现,需要添加的包主要是systemddbus

确认无误后,我们开始运行安装过程:

# 挂载必要的驱动器
mount /dev/sdX /mnt
mount /dev/sdY /mnt/boot

在安装systemd时,可能会遇到文档解压失败的问题。这通常不是关键错误,可以忽略。但为了确保构建日志完整,我们可以手动下载并放置缺失的man手册页。

# 手动下载并放置man手册页
wget [man_pages_url] -P /path/to/packages/

再次运行安装脚本后,检查日志文件。如果日志显示有1833个部分被顺序编译,则表明构建过程正常。

至此,第8章的所有软件包构建完成。


第9章:系统配置

完成第8章后,我们进入第9章的系统配置。与System V需要手动编写大量启动脚本不同,systemd采用更集成的设计理念,许多功能已内置,简化了配置工作。

网络配置

首先配置网络。我们使用静态设备名,并通过DHCP获取IP地址,这对于家庭路由器环境是通用且方便的方式。

  1. 设置网络设备名:我们需要在配置文件中指定设备的MAC地址和期望的名称(如enp0s3)。

    # 示例:/etc/systemd/network/10-eth0.network
    [Match]
    MACAddress=xx:xx:xx:xx:xx:xx
    
    [Network]
    DHCP=ipv4
    
  2. 配置DNS:systemd会生成自己的resolv.conf。为了兼容性,我们创建一个符号链接。

    ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
    
  3. 设置主机名:将主机名写入/etc/hostname文件。

    echo "myhostname" > /etc/hostname
    
  4. 更新hosts文件:至少需要添加localhost条目。

    # /etc/hosts
    127.0.0.1   localhost
    ::1         localhost
    

设备与模块管理概述

这是整个过程中最有趣的部分之一。设备节点并非由内核直接创建在/dev目录下,而是由udev服务根据内核通过sysfs提供的信息动态创建的。

udev规则可以解决设备名随机分配的问题。例如,两个网卡可能因为响应速度不同,在每次启动时被分配不同的名字(如eth0和eth1)。通过udev规则,可以将其固定。

然而,对于初次构建,我们可以暂时接受设备名可能变化的情况,跳过复杂的udev规则编写。

时钟配置

我们可以配置系统的时区和时间同步服务(如systemd-timesyncd),这对于保持系统时间准确很有帮助。本节提供了一些可选的设置项。

控制台配置

这里可以设置键盘布局。例如,德式键盘需要特定的映射。我们可以列出可用的区域设置:

locale -a

输出可能包含de_DE.UTF-8这样的条目。但为了简化,我们可以暂时使用默认的键盘布局。

/etc/inputrc文件用于定义终端按键行为,确保如Home、End、Delete等按键按预期工作。

系统Shell配置

/etc/shells文件列出了系统允许使用的合法登录shell(如/bin/bash/bin/zsh)。这可以防止用户不小心将shell设置为/bin/false等导致无法登录的程序。

其他系统行为配置

  1. 禁用启动时清屏:默认情况下,systemd在启动序列结束后会清屏。我们可以保留启动信息以便查看。

    systemctl disable systemd-vconsole-setup.service
    

    (注:具体命令可能需查阅手册,此处为概念示意)

  2. 临时目录/tmp目录默认被创建为tmpfs(内存文件系统),可以按需修改。

  3. 服务自动重启:systemd可以配置为在服务崩溃后自动重启(例如30秒后),这比System V更健壮。此配置通常针对每个服务单独设置。

  4. 核心转储(Core Dump)限制:可以限制核心转储文件的大小,防止占满磁盘空间。

    # 在 /etc/systemd/coredump.conf 中设置
    [Coredump]
    MaxSize=1G
    

运行完所有配置脚本后,检查生成的文件和链接(如/etc/resolv.conf的符号链接)是否正常。如果一切顺利,第9章的基础配置就完成了。


总结

本节课中,我们一起完成了第8章剩余软件包的安装,并开始了第9章的系统配置。我们学习了如何配置网络(使用DHCP和静态设备名)、了解了udev设备管理的基本概念、并设置了时钟、控制台、Shell以及一些基本的系统行为。

下一节课,我们将进入激动人心的第10章:编译并安装Linux内核以及引导程序GRUB,最终尝试启动我们亲手构建的系统。

015:内核与引导

概述

在本节课中,我们将完成Linux内核的编译与安装,并配置GRUB引导加载程序,最终尝试启动我们构建的系统。

上一节我们完成了系统基础服务和网络的配置。本节中,我们来看看如何编译内核并设置引导。


创建文件系统表

首先,我们需要定义根设备。这是一个ext2设备。我们尚未设置交换分区,因此将其关闭。同时需要访问引导驱动器。

以下是/etc/fstab文件的内容示例:

# 文件系统  挂载点  类型  选项       转储  检查顺序
/dev/sdb2   /       ext2 defaults    0     1
/dev/sdb1   /boot   ext2 defaults    0     2

编译Linux内核

接下来,我们开始编译Linux内核。首先进入内核源码目录。

以下是编译内核的基本步骤:

  1. 清理源码树:执行make mrproper命令。此命令会重置源码树,清除所有之前的构建产物。虽然对于新解压的目录可能不是必须的,但为了避免构建失败,建议执行此步骤。
  2. 生成配置:使用make defconfig命令生成默认配置。这比手动使用make menuconfig进行配置更简单快捷。
  3. 编译内核:执行make命令开始编译内核。
  4. 安装模块:编译完成后,使用make modules_install命令将所有模块和驱动程序安装到目标位置。
  5. 安装内核:使用make install命令将内核安装到/boot目录。

编译过程可能需要一些时间。


配置GRUB引导加载程序

内核安装完成后,我们需要安装引导加载程序。这里使用GRUB。

以下是安装和配置GRUB的步骤:

  1. 安装GRUB:在宿主系统(非LFS环境)中,对目标磁盘(例如/dev/sdb)运行grub-install命令。
    grub-install /dev/sdb
    
    如果系统以UEFI模式启动,可能需要指定目标平台为i386-pc(即传统BIOS模式):
    grub-install --target=i386-pc /dev/sdb
    
  2. 生成GRUB配置文件:创建/boot/grub/grub.cfg文件。以下是一个基本配置示例:
    set timeout=10
    set default=0
    
    menuentry "GNU/Linux, Linux 5.15.12-lfs-11.0" {
        linux /vmlinuz-5.15.12-lfs-11.0 root=/dev/sdb2 ro rootdelay=10 rootwait
    }
    
    • timeout:引导菜单等待时间(秒)。
    • default:默认启动项索引(从0开始)。
    • linux:指定内核文件路径。注意,如果/dev/sdb1挂载到/boot,那么内核文件在设备上的路径就是/vmlinuz-...
    • root:指定根文件系统设备。
    • ro:以只读方式挂载根文件系统。
    • rootdelayrootwait:为解决从USB等慢速设备启动时,内核可能因等待设备就绪超时而导致启动失败的问题,可以添加这些参数。

  1. 设置引导标志:确保引导分区(例如/dev/sdb1)被标记为可引导。可以使用fdisk工具进行设置:
    fdisk /dev/sdb
    # 在fdisk交互界面中,输入 `a` 并选择分区1,然后写入(`w`)退出。
    

测试启动系统

完成上述步骤后,可以尝试启动新系统。

  1. 将构建好的磁盘插入目标机器或配置虚拟机从其启动。
  2. 在BIOS/UEFI设置中,确保从该磁盘启动。
  3. 如果一切顺利,应该能看到GRUB引导菜单,并成功启动内核。
  4. 系统启动后,可以尝试登录并执行基本命令,例如echoping等。

在测试过程中,可能会遇到一些问题,例如网络服务启动失败。这可能是由于缺少必要的固件文件或系统用户配置不匹配导致的。例如,systemd-networkd服务可能需要以特定的系统用户(如systemd-network)运行,如果该用户不存在,服务就会启动失败。解决方法是根据系统需求创建相应的用户和组。


总结

本节课中我们一起学习了如何编译Linux内核、安装GRUB引导加载程序以及配置系统启动。我们成功地从自己构建的系统中启动,并进入了Bash shell。虽然可能遇到如网络服务等小问题,但核心的启动过程已经完成。

构建一个可用的Linux系统主要步骤至此已基本结束。但这只是一个起点。在“Beyond Linux From Scratch”项目中,还可以安装更多软件包,如SSH服务器、图形界面(X11/Wayland)、桌面环境(KDE, GNOME等)来扩展系统的功能。后续可以根据需要继续探索和构建。

016:一小时内仅用BusyBox构建一个极简Linux系统 🚀

在本节课中,我们将学习如何构建一个极其精简的Linux发行版。这个系统非常小,大约只有12兆字节。与完整的“Linux From Scratch”项目不同,我们今天的目标是创建一个仅能提供Shell环境、用于浏览文件系统和启动程序的最小化系统。为了实现这个目标,我们将只构建两个核心包:Linux内核和BusyBox。

概述 📋

一个典型的Linux系统需要两样东西:一个内核(Kernel)和一个根文件系统(Root Filesystem)。内核是系统的核心,负责管理硬件和进程。根文件系统则包含了内核启动后需要运行的所有程序、配置文件和驱动程序。在本教程中,我们的根文件系统将主要由BusyBox构成。

BusyBox是一个特殊的工具,它被称为“瑞士军刀”式的嵌入式Linux工具。它将许多常见的Unix工具(如lscpecho等)的功能集成到了一个单一的可执行文件中。这极大地减少了系统所需的空间和依赖。

接下来,我们将分步完成内核编译、BusyBox静态编译以及根文件系统的构建,最终将它们组合成一个可以运行的Linux系统。

第一步:准备构建脚本与环境 🛠️

我们将创建一个脚本来自动化整个构建过程。首先,定义我们要使用的软件版本,并设置一些变量。

#!/bin/bash

# 定义要使用的版本
KERNEL_VERSION="5.15.6"
BUSYBOX_VERSION="1.34.1"

# 提取内核的主版本号(例如从“5.15.6”中提取“5”)
KERNEL_MAJOR_VERSION=$(echo $KERNEL_VERSION | sed -E 's/^([0-9]+)[^0-9].*$/\1/')

我们创建src目录作为工作空间,并进入该目录。

# 创建并进入源码目录
mkdir -p src
cd src

第二步:下载与编译Linux内核 ⚙️

首先,我们需要下载指定版本的Linux内核源代码。

# 下载并解压Linux内核
wget -q --show-progress https://cdn.kernel.org/pub/linux/kernel/v$KERNEL_MAJOR_VERSION.x/linux-$KERNEL_VERSION.tar.xz
tar -xf linux-$KERNEL_VERSION.tar.xz
cd linux-$KERNEL_VERSION

进入内核源码目录后,我们使用默认配置并开始编译。make defconfig会生成一个适合当前架构的默认配置。make -j8会启动并行编译以加快速度。

# 配置并编译Linux内核
make defconfig
make -j8 || exit 1

编译完成后,我们将生成的内核映像文件arch/x86/boot/bzImage复制到项目根目录。

# 复制编译好的内核到项目根目录
cp arch/x86/boot/bzImage ../../kernel.img
cd ..

上一节我们完成了内核的编译,本节中我们来看看如何构建BusyBox。

第三步:下载与静态编译BusyBox 📦

现在,我们来处理BusyBox。首先下载并解压源代码。

# 下载并解压BusyBox
wget -q --show-progress https://busybox.net/downloads/busybox-$BUSYBOX_VERSION.tar.bz2
tar -xf busybox-$BUSYBOX_VERSION.tar.bz2
cd busybox-$BUSYBOX_VERSION

我们的目标系统没有动态链接库(如glibc),因此需要将BusyBox编译成静态二进制文件,将所有依赖库都打包进可执行文件内部。

在基于Arch Linux的系统(如Manjaro)上,直接静态编译可能会遇到库缺失的问题。一个解决方案是使用musl libc这个替代的C标准库。我们需要安装相关工具链。

# 对于Manjaro/Arch系统,安装musl工具链(在其他发行版上可能不需要)
sudo pacman -S --noconfirm musl binutils-musl 2>/dev/null || true

接下来,我们生成默认配置,并修改配置以启用静态编译。我们使用sed命令自动修改配置文件。

# 生成默认配置并修改为静态编译
make defconfig
sed -i 's/^CONFIG_STATIC.*$/CONFIG_STATIC=y/' .config

然后,我们使用musl-gcc编译器(如果已安装)来执行静态编译。

# 使用musl-gcc进行静态编译(如果可用),否则使用系统gcc
if command -v musl-gcc &> /dev/null; then
    make CC=musl-gcc -j8 || exit 1
else
    make -j8 || exit 1
fi

编译成功后,BusyBox的可执行文件busybox就生成了。

第四步:构建初始内存盘(initrd)文件系统 🗂️

内核需要一个根文件系统来挂载。我们将创建一个初始内存盘(initrd),它是一个临时的根文件系统,在系统启动早期被加载到内存中。

首先,创建一个目录结构来模拟根文件系统。

# 创建initrd目录结构
cd ../..
mkdir -p initrd
cd initrd
mkdir -p bin dev proc sys

将编译好的静态busybox二进制文件复制到bin目录下。

# 复制busybox到bin目录
cp ../src/busybox-$BUSYBOX_VERSION/busybox bin/

BusyBox的一个巧妙特性是:它会根据自己被调用的名字(argv[0])来执行不同的功能。例如,如果创建一个指向busybox的名为ls的符号链接,那么执行这个链接就相当于执行ls命令。

以下是创建所有必要符号链接的步骤:

# 进入bin目录并为busybox创建所有applet的符号链接
cd bin
for applet in $(./busybox --list); do
    ln -s busybox $applet
done
cd ..

现在,我们需要创建一个初始化脚本init。这个脚本是内核启动后运行的第一个用户空间进程(PID 1)。

# 创建初始化脚本 init
cat > init << 'EOF'
#!/bin/sh
# 挂载必要的虚拟文件系统
mount -t devtmpfs devtmpfs /dev
mount -t proc proc /proc
mount -t sysfs sysfs /sys

# 设置内核日志级别
echo 0 > /proc/sys/kernel/printk

# 启动一个shell
exec /bin/sh
EOF

# 使init脚本可执行
chmod +x init

最后,使用cpio命令将整个initrd目录打包成一个镜像文件。

# 将initrd目录打包成cpio归档文件(镜像)
find . | cpio -o -H newc > ../initrd.img
cd ..

第五步:使用QEMU运行系统 🖥️

现在,我们拥有了两个关键文件:kernel.img(内核)和initrd.img(初始内存盘)。我们可以使用QEMU虚拟机来启动这个系统。

以下是用QEMU启动系统的命令:

# 使用QEMU运行构建好的系统(图形界面模式)
qemu-system-x86_64 -kernel kernel.img -initrd initrd.img

如果你想在终端中直接与系统交互,而不弹出图形窗口,可以使用以下命令:

# 使用QEMU运行构建好的系统(无图形界面,使用串口控制台)
qemu-system-x86_64 -kernel kernel.img -initrd initrd.img -nographic -append "console=ttyS0"

在Shell中,你可以尝试一些基本命令,如lscdecho等。要退出QEMU,在图形界面中可以关闭窗口,在无图形模式下的终端中,可以按Ctrl+A,然后按X来强制退出。

总结 🎉

在本节课中,我们一起完成了一个极简Linux系统的构建。我们学习了:

  1. 下载和编译Linux内核:使用make defconfigmake -j8
  2. 静态编译BusyBox:理解了静态链接与动态链接的区别,并解决了在特定发行版上的编译问题。
  3. 构建初始内存盘(initrd):创建了包含BusyBox和基本目录结构的根文件系统,并编写了启动脚本init
  4. 使用QEMU测试系统:将内核和initrd组合,在虚拟机中成功启动了一个功能完整的微型Linux环境。

整个构建过程被浓缩在了一个60多行的脚本中,这充分展示了Linux模块化设计的强大与简洁。通过这个实践,你不仅得到了一个可运行的系统,更深入理解了Linux系统启动的基本原理和核心组件是如何协同工作的。希望这为你进一步探索操作系统世界打下了坚实的基础。

posted @ 2026-03-29 09:35  布客飞龙I  阅读(6)  评论(0)    收藏  举报