系列 · Linux · 第 4 篇

Linux(四):软件包管理

跨发行版的包管理实战:Debian/Ubuntu 的 dpkg/apt、RHEL/Rocky 的 rpm/dnf、Arch 的 pacman,依赖排障、版本锁定、镜像源、源码编译与 Snap/Flatpak/AppImage 等现代方案。

很多人学 Linux 包管理,一开始接触的无非是三个动作:install(安装)、remove(删除)、upgrade(升级)。日常用起来似乎也够了,但总有一天会遇到麻烦——依赖冲突导致无法安装、升级后服务无法启动、内核更新后系统无法进入,或在国内拉取镜像时长时间卡住。到了这个时候,仅靠死记硬背命令已不足以应对问题,你需要建立一个清晰的心智模型:一个里到底有什么?包管理器在背后究竟在做什么?它的状态存放在哪里? Debian 的 apt/dpkg 和 Red Hat 的 dnf/rpm 在凌晨两点的生产环境上,到底有哪些相同点,又有哪些不同点?

Linux(四):软件包管理 — 章节概览图

这篇文章的目标就是提供这样的模型,同时附上实用的操作指南。我们会从头开始讲起:.deb.rpm 文件里到底装了什么?为什么需要包管理器?接着,我们会把 aptdnfpacman 摆在一起横向对比,不只是列出「等价命令」,而是深入探讨它们的设计理念和行为差异——比如依赖解析的方式、版本锁定的机制、降级操作的支持,以及对仓库信任链的处理。最后,我们还会分享一些只能靠踩坑才能学到的经验:如何正确配置国内镜像源、如何从源码编译 Nginx (并解释清楚 configure/make/make install 这三步的本质作用)、如何在不依赖 apt 的情况下部署 JDK 二进制压缩包,以及一套保障生产环境机器长期处于「可升级」状态的运维实践。

主流 Linux 包管理工具链


一个包里到底有什么#

软件包是一个单独的文件(例如 .deb.rpm.pkg.tar.zst),它不仅包含了某个软件运行所需的所有内容,还附带了描述如何安装的元数据。正是这些元数据让一个压缩文件变成了真正的软件包——如果没有元数据,那它就只是一个普通的 tarball。

具体来说,一个典型的服务器端软件包通常包含以下六类内容:

1. 可执行文件: 这些是编译好的二进制程序,通常放在 /usr/bin(普通用户命令)或 /usr/sbin(系统管理命令)目录下。比如 /usr/bin/vim/usr/sbin/nginx/usr/bin/gcc

2. 配置文件: 软件自带的默认配置文件,几乎都位于 /etc 目录下。包管理器对这些文件有特殊处理:升级时默认不覆盖已被修改的配置文件。apt 会在升级时提示你选择(Y/I/N/O/D/Z),并将新版本的配置另存为 .dpkg-dist;而 dnf 则会把新配置保存为 .rpmnew,保留你的原始文件不动。该机制有效避免了大量线上故障。

1
2
3
/etc/nginx/nginx.conf       # nginx 配置
/etc/mysql/my.cnf           # MySQL 配置
/etc/ssh/sshd_config        # SSH 守护进程配置

3. 共享库: 这些是程序运行时动态链接的 .so 文件,相当于 Windows 的 DLL 文件,通常存放在 /usr/lib/lib 或特定发行版的路径下,比如 /lib/x86_64-linux-gnu/。共享库的好处显而易见:节省磁盘和内存(一份库供所有进程使用),并且只需升级一次 libssl3 就能修复全系统所有 TLS 客户端的安全问题。

1
2
/usr/lib/x86_64-linux-gnu/libssl.so.3   # OpenSSL
/lib/x86_64-linux-gnu/libc.so.6         # GNU C 库

4. 数据文件: 程序运行时需要的非代码、非配置文件,比如 /var/lib/mysql 下的空数据库模板、/var/www/html 下的默认网站根目录、 locale 数据、示例证书等。

5. 文档: 包括 /usr/share/man/ 下的手册页,以及 /usr/share/doc/<pkg>/ 下的 README 和 changelog 文件。遇到问题时,这里往往是比搜索引擎更靠谱的第一手资料。

6. systemd 服务单元文件。 如果软件包包含守护进程,它会将 .service 文件放置在 /usr/lib/systemd/system/ 目录下,这样安装完成后就可以直接通过 systemctl enable nginx 启用服务。这些文件由软件包管理,管理员如果需要自定义,则可以将覆盖文件放在 /etc/systemd/system/ 下,避免升级时被覆盖。

除了上述内容,软件包的元数据还记录了以下信息:包名与版本、依赖关系(.deb 中叫 Depends:.rpm 中叫 Requires:)、冲突关系、安装后脚本(比如 useradd nginx),以及每个文件的 SHA256 校验值。最后这一点使得 dpkg -Vrpm -V 能够检测出哪些文件被篡改过。

为什么我们需要包管理器?#

如果没有包管理器,每次安装软件都需要手动完成以下步骤:

  1. 找到下载链接(还得祈祷源站是可信的);
  2. 手动解决依赖关系——A 需要 B 1.1+,B 需要 C,而 C 又和已安装的 D 冲突;
  3. 决定每个文件应放在哪个目录;
  4. 记录所有复制的文件,以便日后彻底卸载;
  5. 手动定期检查上游是否有安全更新。

而包管理器通过一个数据库(如 /var/lib/dpkg/rpmdb)和一个求解器,将这些繁琐的工作自动化。数据库记录了每个已安装包所拥有的文件,求解器则知道仓库中有哪些包、依赖关系如何连接,从而确保安装、升级或卸载操作始终是对数据库的一致性变更。这就是包管理器的核心价值所在——其核心价值正在于此。

主流的几个工具链#

Linux 的包管理系统早在几十年前就分化成了几个主要家族,此后一直保持着相对稳定的格局。以下是需要掌握的核心内容:

家族常见发行版包格式底层工具高层工具
DebianDebian / Ubuntu / Mint.debdpkgaptapt-get
Red HatRHEL / CentOS / Rocky / Alma / Fedora.rpmrpmdnf(EL7 上是 yum
ArchArch / Manjaro / EndeavourOS.pkg.tar.zstpacmanpacmanyay
SUSEopenSUSE / SLES.rpmrpmzypper
GentooGentoo源码 ebuild-portageemerge

在实际使用中,真正需要区分的不是这些家族本身,而是底层工具和高层工具的区别。底层工具(如 dpkgrpm、直接用 pacman -U)只处理单个文件:解压、执行脚本、更新本地数据库——但它们不会主动去解决依赖问题或从远程仓库拉取缺失的依赖。而高层工具(如 aptdnfpacman -Szypper)则在底层工具的基础上增加了依赖解析功能,并能够与远程仓库通信。绝大多数情况下,高层工具才是你的首选;只有在高层工具无法满足需求时,比如强制安装一个理论上存在冲突的包,或者拆开一个 .deb 文件查看其内容,才会需要用到底层工具。

接下来的内容将重点围绕 apt(Debian/Ubuntu)、dnf(RHEL/Rocky/Fedora)和 pacman(Arch)展开,并在必要时补充底层工具 dpkgrpm 的相关用法。

依赖求解:这才是真正难的部分#

依赖解析图

包管理器最让人意外的地方在于,它的核心代码并不在「安装逻辑」上,而是在依赖求解器里。当你运行 apt install nginx 时,背后实际上需要完成以下复杂的工作:

  1. 读取元数据:从缓存的 Packages 索引中提取 nginx 的相关信息;
  2. 遍历依赖树nginx 依赖 libssl3libpcre2-8zlib1glibc6 等库;每个依赖项又可能依赖其他库,直到找到系统中已经安装且版本符合要求的为止;
  3. 统一版本冲突:如果依赖树中有两条路径分别要求 libc6 的不同版本,求解器必须找到一个能满足双方需求的版本,或者直接报告冲突。共享库无法同时安装多个版本——动态链接器是按名称解析符号的,而不是按路径,最终只会绑定到一个版本;
  4. 确定安装顺序:确保所有依赖项都按照正确的顺序安装,避免安装后脚本运行时找不到必要的依赖;
  5. 明确失败状态:如果无法找到一致的解决方案,直接报错退出,而不是留下半成品导致系统处于不一致的状态。

如果你跳过高层工具,直接操作底层命令,这些复杂的逻辑就全都没了:

1
2
3
4
5
sudo dpkg -i nginx_1.24.0-1_amd64.deb
# dpkg: 处理 nginx 时出错: 依赖问题:
#  nginx 依赖 libssl3 (>= 3.0.0); 然而:
#   未安装 libssl3。
sudo apt install -f         # 让 apt 来修复 dpkg 引发的问题

这里的 apt install -f 实际上是在事后调用依赖求解器,尝试修复部分安装导致的混乱状态。其实完全可以通过高层工具一步到位地处理本地文件,避免这种麻烦:

1
2
sudo apt install ./nginx_1.24.0-1_amd64.deb     # apt 会自动解析仓库中的依赖
sudo dnf install ./nginx-1.24.0-1.x86_64.rpm    # Red Hat 系列同理

同样的逻辑也适用于卸载。假设你安装了 nginx 和它的扩展库 libcache-extra,单独卸载 libcache-extra 是没问题的;但如果要卸载 nginx,那么那些只为 nginx 安装的依赖(比如 libpcre2-8)如果没有其他软件使用,就应该一并清理掉。这就是 apt autoremovednf autoremove 的作用所在。

Debian / Ubuntu: apt 和 dpkg#

什么时候用哪个工具?#

工具它是什么?适合场景
apt现代化的用户友好前端,支持彩色输出、进度条显示和合理的默认设置。日常交互式操作,比如终端会话中使用。
apt-get老牌前端工具,功能与 apt 类似,但输出更稳定且易于脚本解析。Shell 脚本、 Ansible、 Dockerfile 等自动化场景。
dpkg底层工具,一次只能处理一个 .deb 包,不解决依赖关系。检查包内容、强制安装或修复系统状态。

简单理解:aptapt-get 的增强版,增加了交互友好的特性;apt-get 是一个依赖管理器,包裹着 dpkg;而 dpkg 才是真正负责解压文件并运行维护脚本的核心工具。

常用命令#

1
2
3
4
5
6
7
8
sudo apt update                       # 刷新软件源索引(不会安装任何东西)
sudo apt upgrade                      # 升级所有可更新的包,但不会删除任何现有包
sudo apt full-upgrade                 # 升级时允许删除旧包以解决冲突
sudo apt install nginx                # 安装 nginx 及其依赖
sudo apt install nginx=1.24.0-2ubuntu1   # 安装指定版本的 nginx
sudo apt remove nginx                 # 卸载 nginx,但保留配置文件(/etc/nginx)
sudo apt purge nginx                  # 卸载 nginx 并彻底清除配置文件
sudo apt autoremove                   # 清理不再需要的依赖包

新手最容易误解的两个命令:

  • apt update 不会安装任何东西。它的作用是从镜像源下载最新的 Packages 索引文件,为后续的 apt install 提供可用信息。「明明仓库里有这个包,却装不上」的问题,通常是因为忘了运行 update
  • apt remove 会保留配置文件。这通常是大家想要的行为(重新安装时可以恢复之前的配置)。但如果你要彻底移除某个服务,记得用 apt purge

搜索、检查和定位#

1
2
3
4
5
6
7
apt search nginx                  # 在已配置的软件源中搜索 nginx
apt show nginx                    # 查看 nginx 的版本、依赖、描述和大小等信息
apt-cache policy nginx            # 显示可安装的候选版本及其来源
dpkg -l                           # 列出系统上所有已安装的包
dpkg -l | grep nginx              # 检查 nginx 是否已安装
dpkg -L nginx                     # 列出 nginx 安装的所有文件
dpkg -S /usr/sbin/nginx           # 查找某个文件属于哪个包

最后两条命令是运维排障的利器。「这个包到底在我的系统里装了哪些文件?」或者「PATH 中这个莫名其妙的二进制文件是哪个包带的?」这些问题在排查问题时经常遇到,而 Linux 上没有比 dpkg -S 更好用的工具来反查文件归属。

锁定版本、降级和冻结#

1
2
3
4
5
6
7
8
sudo apt-mark hold nginx          # 禁止 nginx 自动升级
apt-mark showhold                 # 查看当前被锁定的包
sudo apt-mark unhold nginx        # 解除对 nginx 的锁定

# 降级三步走
apt-cache policy nginx            # 1) 查看可用版本
sudo apt install nginx=1.22.1-1ubuntu1   # 2) 安装指定的老版本
sudo apt-mark hold nginx          # 3) 锁定版本,防止下次升级覆盖

当你发现上游发布了一个有问题的版本时,hold 就派上用场了:先钉住老版本,等到合适的维护窗口再升级,避免半夜被自动更新搞砸。

清理无用文件#

1
2
3
sudo apt clean        # 删除 /var/cache/apt/archives 下的所有缓存包
sudo apt autoclean    # 仅删除那些已经不在任何源中的过期缓存包
sudo apt autoremove   # 清理不再被依赖的孤儿包

我自己常用的一条组合命令,直接记在肌肉记忆里:

1
sudo apt update && sudo apt upgrade -y && sudo apt autoremove -y && sudo apt autoclean

每周在每台机器上跑一次,保证 /var 分区不会被无用的缓存文件撑爆。

RHEL / Rocky / Fedora: dnf (顺带 yum 和 rpm)#

从 CentOS 8 / RHEL 8 和 Fedora 22 开始,dnf 取代了 yum。虽然命令名几乎完全一致(这是为了保持兼容性),但底层实现已经用 Python 重写,并引入了一个真正的 SAT 求解器(libsolv)。这使得 dnf 在处理大规模事务时明显更快。 CentOS 7 仍然使用 yum,但在其他场景下都可以默认使用 dnf

常用命令#

1
2
3
4
5
6
7
8
sudo dnf makecache                 # 更新元数据缓存
sudo dnf check-update              # 查看可升级的软件包
sudo dnf upgrade                   # 升级所有软件包
sudo dnf install nginx             # 安装软件包
sudo dnf install nginx-1.24.0      # 安装指定版本
sudo dnf remove nginx              # 卸载软件包
sudo dnf autoremove                # 清理不再需要的依赖
sudo dnf downgrade nginx-1.22.1    # 回退到旧版本(apt 没有类似功能)

dnf downgradednf 相较于 apt 的一大优势。在 apt 的世界里,降级通常意味着「手动安装旧版本并祈祷不冲突」,而在 dnf 中,降级是一个原生支持的操作,简单且可靠。

搜索、检查与定位#

1
2
3
4
5
6
7
8
9
dnf search nginx                   # 搜索软件包
dnf info nginx                     # 查看详细信息
dnf list --showduplicates nginx    # 列出所有可用版本
rpm -qa                            # 列出所有已安装的软件包
rpm -qa | grep nginx               # 检查是否安装了某个软件包
rpm -ql nginx                      # 查看某个软件包安装的所有文件
rpm -qf /usr/sbin/nginx            # 查询某个文件属于哪个软件包
rpm -qc nginx                      # 仅列出配置文件
rpm -V nginx                       # 校验文件是否被篡改

rpm -V 是一个容易被忽视的强大工具。比如输出 S.5....T. c /etc/nginx/nginx.conf 表示:文件大小(S)、 MD5 校验值(5)和修改时间(T)发生了变化,同时它是一个配置文件(c)。配置文件被修改是正常的,但如果 /usr/sbin/ 下的二进制文件发生变化,那很可能是系统被入侵的迹象。

版本锁定与历史记录#

1
2
3
4
5
6
7
8
sudo dnf install 'dnf-command(versionlock)'   # 安装版本锁定插件
sudo dnf versionlock add nginx                # 锁定某个软件包版本
dnf versionlock list                          # 查看已锁定的软件包
sudo dnf versionlock delete nginx             # 解除锁定

dnf history                                   # 查看所有操作历史
dnf history info 42                           # 查看第 42 次操作的详细信息
sudo dnf history undo 42                      # 回滚第 42 次操作

dnf historydnf 的一大亮点,apt 并没有类似的功能。它会记录每次操作的时间戳、命令行参数以及完整的变更内容,并允许随时回滚到任意一次操作。对于那些经历了多任管理员的服务器来说,这个功能是还原「到底发生了什么」的关键工具。

Arch: pacman#

在 Arch Linux 中,pacman 是一个兼具底层和高层功能的包管理工具。 Arch 采用滚动更新模式,没有类似「Arch 22.04」这样的版本号,系统状态完全取决于当前仓库的内容。因此, Arch 的工作流更倾向于「始终保持整体更新」。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
sudo pacman -Syu                  # 同步仓库并升级所有软件包(Arch 的核心命令)
sudo pacman -S nginx              # 安装软件包
sudo pacman -Ss nginx             # 搜索软件包
sudo pacman -Si nginx             # 查看仓库中软件包的详细信息
sudo pacman -Qi nginx             # 查看已安装软件包的详细信息
sudo pacman -Ql nginx             # 列出软件包安装的所有文件
sudo pacman -Qo /usr/bin/nginx    # 查询某个文件属于哪个软件包
sudo pacman -R nginx              # 卸载软件包(保留依赖)
sudo pacman -Rns nginx            # 卸载软件包 + 清理孤立依赖 + 删除配置文件
sudo pacman -Sc                   # 清理缓存

Arch 的核心原则是:切勿进行部分升级。如果你几周没有运行 pacman -Syu,直接执行 pacman -S nginx,可能会安装一个基于新库编译的 nginx,而你的系统上还停留在旧库版本,最终导致 nginx 无法正常运行。因此,在安装新软件之前,务必先同步并升级整个系统,或者直接使用 pacman -Syu nginx 一步完成。

Arch 用户仓库(AUR)是一个由社区维护的构建脚本集合,前端工具如 yayparupacmanmakepkg 结合起来,提供了一键拉取源码、编译和安装的功能。这些工具非常方便,但请记住:对于 AUR 中的内容,一定要抱着「先读 PKGBUILD,再决定是否安装」的态度来使用。

一台机器上的包生命周期#

包生命周期

抛开具体的工具不谈,系统上的每个软件包都会经历一个相同的生命周期:查找 → 安装 → 更新(或锁定版本)→ 卸载。包管理器会通过数据库记录每个包的当前状态,比如 Debian 系统中是 /var/lib/dpkg/status,而 Red Hat 系统则是 /var/lib/rpm/ 目录下的 rpmdb。你执行的每一条命令,本质上都是对这个数据库的一次事务操作,同时伴随着磁盘上的实际文件变动。

这就是为什么「我直接删掉了某个二进制文件」这种做法总是不对的。在数据库里,这个包的状态仍然是「已安装」,下次更新时,包管理器会默默地把被删除的文件重新放回去。而当你运行 dpkg -Vrpm -V 时,它们会报告该文件「缺失」。正确的做法是:要么彻底卸载这个包(比如用 apt purgednf remove),要么——如果你确实需要保留包的元数据但屏蔽某个文件——使用像 dpkg-divert 这样的正规手段来处理。

仓库到底长什么样#

仓库结构与信任链

当你运行 apt update 时,系统会通过 HTTPS 连接到镜像站点,并按照特定的目录结构下载索引文件。熟悉这个结构后,排查镜像问题就变得简单了,只需要用 curl 抓取几个关键 URL 就能定位问题。

Debian 风格的 apt 仓库包含两部分核心内容:

  • dists/<suite>/:每个发行版套件(suite)的元数据。最顶层的文件是 Release(或者它的内联签名版本 InRelease),其中列出了所有组件(如 mainuniverse 等)和架构,并附带对应 Packages.gz 索引文件的 SHA256 校验值。Release.gpg 是分离的 GPG 签名文件,作为信任链的起点。
  • pool/:实际存放 .deb 包的地方,按照源码包名称组织目录结构,例如 pool/main/n/nginx/nginx_1.24.0-1_amd64.deb

安装软件包(如 nginx)的信任链如下:

  1. 你的机器上存储了一组受信任的 GPG 公钥,位于 /etc/apt/trusted.gpg.d/ 目录下(这些公钥通常通过源列表中的 signed-by= 字段指定,或者通过旧版的 apt-key 工具添加)。
  2. 执行 apt update 时,系统会下载 InRelease 文件,并使用上述公钥验证其签名。如果验证失败,整个套件将被拒绝使用(这就是常见的 NO_PUBKEY 错误的来源)。
  3. 验证通过后,系统从 InRelease 文件中获取每个组件的 Packages.gz 文件的 SHA256 校验值,下载并校验这些索引文件。
  4. 接着,从 Packages.gz 中提取目标软件包(如 nginx_1.24.0-1_amd64.deb)的 SHA256 校验值,从 pool/ 目录下载该文件并进行校验,只有在所有校验都通过后,才会将其交给 dpkg 处理。

如果信任链上的任何一环出现问题——比如缺少公钥、Release 文件被篡改、.deb 文件校验不匹配——apt 都会终止操作。这就是它的安全保障:即使恶意镜像可以提供任意字节流,也无法绕过信任链,除非攻击者同时掌握了上游的签名密钥。

Red Hat 系列的结构与此类似:repodata/repomd.xml 是签名的清单文件,其中包含了 primary.xml.gz 索引文件和各个 .rpm 包的校验信息。dnf 默认强制执行这条信任链,除非你显式关闭 gpgcheck(强烈建议不要这么做)。

配置国内镜像源#

在国内,默认的 Ubuntu 和 CentOS 源速度较慢,解决办法是切换到国内镜像。目前两大主流镜像是阿里云(mirrors.aliyun.com)和清华大学开源软件镜像站(mirrors.tuna.tsinghua.edu.cn),它们都支持主流发行版。

Ubuntu (apt):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 1. 备份原始配置文件
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak

# 2. 替换为阿里云镜像地址(以 Ubuntu 22.04 'jammy' 为例)
sudo sed -i.aliyun \
    -e 's|http://.*archive.ubuntu.com|http://mirrors.aliyun.com|g' \
    -e 's|http://.*security.ubuntu.com|http://mirrors.aliyun.com|g' \
    /etc/apt/sources.list

# 3. 刷新缓存并验证
sudo apt update
grep mirrors.aliyun.com /etc/apt/sources.list   # 检查是否替换成功

常见 Ubuntu LTS 版本的代号分别是:bionic(18.04)、focal(20.04)、jammy(22.04)、noble(24.04)。确保在配置文件中填写正确的代号。

从 Ubuntu 24.04 开始,源配置文件从 /etc/apt/sources.list 移到了 /etc/apt/sources.list.d/ubuntu.sources,格式也改为 deb822。不过,同样的 sed 命令依然适用,只需针对新文件操作即可。

CentOS / Rocky (dnf):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 备份原始配置文件
sudo cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.bak

# CentOS 7
sudo curl -o /etc/yum.repos.d/CentOS-Base.repo \
    http://mirrors.aliyun.com/repo/Centos-7.repo

# Rocky Linux 9(CentOS 8/Stream 已停止维护,生产环境建议使用 Rocky 或 Alma)
sudo sed -e 's|^mirrorlist=|#mirrorlist=|g' \
         -e 's|^#baseurl=http://dl.rockylinux.org/$contentdir|baseurl=https://mirrors.aliyun.com/rockylinux|g' \
         -i.bak /etc/yum.repos.d/rocky-*.repo

# 清理缓存并生成新缓存
sudo dnf clean all && sudo dnf makecache
dnf repolist                                # 确认 aliyun 镜像已生效

清华大学镜像站的路径与阿里云完全一致,只需将域名替换为 mirrors.tuna.tsinghua.edu.cn 即可。根据你的网络情况选择更快的镜像。配置完成后,务必运行一次完整的升级命令(apt upgrade -ydnf upgrade -y),以便及时发现问题。这样可以避免在真正需要安装软件时才发现镜像未生效。

源码编译: configure / make / make install#

有时候,仓库里的版本太旧了,或者你需要一个发行版包中没有的编译选项(比如某个 Nginx 模块、特定版本的 OpenSSL 或者某些调优参数)。这种情况下,从源码构建就成了备选方案。经典的 autotools 三步流程是这样的:

  1. ./configure:探测你的系统环境——用的是什么编译器、有哪些库和头文件,然后根据你的需求生成一个 Makefile
  2. make:按照生成的 Makefile 调用编译器,把源代码编译成可执行文件。
  3. sudo make install:将编译好的文件安装到 Makefile 中指定的目录(通常是通过 --prefix 参数指定的路径)。

CMake 和 Meson 的命令虽然不同(比如 cmake -B buildcmake --build buildcmake --install build),但整体流程是一样的:先配置(configure),再编译(build),最后安装(install)。

实战案例:在 Ubuntu 上编译 Tengine#

Tengine 是淘宝维护的一个 Nginx 分支,增加了许多额外模块。它的构建流程和上游的 Nginx 完全一致——如果你能编译 Tengine,那么编译 Nginx 也不在话下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1. 安装编译依赖
sudo apt install -y build-essential libpcre3 libpcre3-dev libssl-dev zlib1g-dev
# Red Hat 系的等价命令:
# sudo dnf install -y gcc make pcre-devel openssl-devel zlib-devel

# 2. 下载源码
wget http://tengine.taobao.org/download/tengine-2.3.3.tar.gz
tar -zxvf tengine-2.3.3.tar.gz
cd tengine-2.3.3

# 3. 配置:在这里指定编译选项
./configure \
    --prefix=/usr/local/nginx \
    --with-http_ssl_module \
    --with-http_v2_module \
    --with-http_realip_module \
    --with-http_gzip_static_module

# 4. 编译:利用所有 CPU 核心并行编译
make -j"$(nproc)"

# 5. 安装:将二进制文件、默认配置和 HTML 文件拷贝到指定目录
sudo make install

每一步具体做了什么:

  • ./configure 检查你的系统是否具备 C 编译器以及 OpenSSL、 PCRE 和 zlib 的开发头文件。如果缺少某些依赖,它会明确报错——你只需要安装对应的 -dev 包,然后重新运行即可。接着,它会生成一个 Makefile,将 /usr/local/nginx 写入作为安装路径,并启用你指定的四个 HTTP 模块。
  • make 按照 Makefile 的指示进行编译,最终生成 objs/nginx 以及一些辅助文件。
  • sudo make install 创建 /usr/local/nginx/{sbin,conf,logs,html} 目录,并将二进制文件、默认配置和示例 HTML 文件拷贝进去。需要注意的是,它不会为你创建 systemd 服务单元、注册用户或开放防火墙端口——这些都需要你自己手动完成。

运行起来很简单:

1
2
3
/usr/local/nginx/sbin/nginx                    # 启动
/usr/local/nginx/sbin/nginx -s reload          # 重载配置
/usr/local/nginx/sbin/nginx -s stop            # 停止

如果想让 Nginx 由 systemd 管理,可以创建一个 /etc/systemd/system/nginx.service 文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[Unit]
Description=Nginx HTTP Server (built from source)
After=network.target

[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStart=/usr/local/nginx/sbin/nginx
ExecReload=/usr/local/nginx/sbin/nginx -s reload
ExecStop=/usr/local/nginx/sbin/nginx -s stop
PrivateTmp=true

[Install]
WantedBy=multi-user.target

然后执行以下命令:

1
2
sudo systemctl daemon-reload
sudo systemctl enable --now nginx

从源码编译的最大代价是:没有 apt upgrade 替你管理更新。升级路径、安全补丁跟踪、以及当 OpenSSL 更新时重新编译整个软件的工作,都得你自己负责。对于大多数生产环境来说,正确的答案通常是「除非有明确的理由,否则优先使用发行版提供的包」。而从源码编译,则是用来解决那些「明确理由」的。

二进制压缩包:以 JDK 为例#

有些软件直接以「解压即用」的二进制压缩包形式发布,比如 JDK、 Go、 Node.js 以及大多数数据库引擎。这种方式没有安装程序来限制你,文件存放位置和环境变量配置完全由你自己决定。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# 1. 下载(Adoptium / Oracle / 内网镜像)
wget https://download.example.com/jdk-17_linux-x64_bin.tar.gz

# 2. 解压到 /opt——这是存放「第三方独立软件」的惯例路径
sudo tar -zxf jdk-17_linux-x64_bin.tar.gz -C /opt
sudo mv /opt/jdk-17* /opt/jdk-17                # 调整为简洁且稳定的路径

# 3. 配置全局环境变量
sudo tee /etc/profile.d/jdk17.sh > /dev/null <<'EOF'
export JAVA_HOME=/opt/jdk-17
export PATH=$JAVA_HOME/bin:$PATH
EOF

# 4. 让当前 shell 立即生效
source /etc/profile.d/jdk17.sh

# 5. 验证安装
java -version
javac -version

/etc/profile.d/*.sh 文件会在所有用户的登录 shell 中被加载,这正是 JDK 这类工具需要的作用范围。如果只需要为当前用户配置,可以将内容写入 ~/.bashrc~/.zshrc。如果需要同时管理多个 JDK 版本,可以将它们统一放在 /opt/jdk-XX 目录下,通过切换 JAVA_HOME 来选择版本(或者使用 sdkmanjenv 等工具)。

类似的套路也适用于 Go (/opt/go)、 Node.js (/opt/node-vXX)等工具。这些工具之所以广受欢迎,正是因为它们绕过了 Linux 发行版的更新周期——当你急需某个上游版本,而发行版的版本落后半年时,这种方式就显得尤为实用。

现代方案: Snap、 Flatpak、 AppImage#

Snap、Flatpak、AppImage 与发行版包管理的对比

传统的发行版包管理模式有一个长期存在的短板:它将软件紧密绑定到系统的其他部分。比如,一个基于 Ubuntu 22.04 的 glibcgtk 编译的 Firefox,很难直接移植到 RHEL 8 上运行。针对这个问题,有三种现代方案提供了不同的解决思路:

  • Snap(由 Canonical 开发):将所有依赖打包到一个 .snap 文件中(实际上是 SquashFS 镜像),通过 AppArmor 提供沙箱隔离,并由 snapd 自动更新。 Ubuntu 上的一些应用(如 Firefox 和 Chromium 的 snap 版本)默认使用 Snap,但在其他发行版上支持较少。
  • Flatpak(由 freedesktop.org 和 RHEL 社区推动):基于共享的 runtime(例如 org.freedesktop.Platform//22.08)构建,使用 bubblewrap 实现沙箱隔离,主要通过 Flathub 分发。目前已成为 Linux 桌面 GUI 应用的事实标准。
  • AppImage:一个独立的可执行文件,无需安装,不依赖守护进程,默认也不提供沙箱。双击即可运行,非常适合「快速试用某个工具」或分发内部工具。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 安装方法
sudo apt install snapd                                  # 一次性设置
sudo snap install firefox

sudo dnf install flatpak                                # 一次性设置
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install flathub org.gimp.GIMP

# AppImage:下载后赋予执行权限即可运行
wget https://example.com/foo.AppImage
chmod +x foo.AppImage
./foo.AppImage

如何选择合适的方案?

  • 服务器端软件(如 nginxmysqlpythonredis):优先选择发行版自带的包管理工具。它们体积更小,与 systemd 集成良好,安全更新也由发行版的安全团队负责推送。
  • 桌面 GUI 应用,尤其是更新频率高于发行版的:推荐使用 Flatpak (Flathub 提供了最丰富的选择)或 Snap。
  • 临时测试工具,或者不想污染系统环境的: AppImage 是最佳选择。

这四种方案可以同时存在于同一台机器上。它们分别安装在独立的根目录下,使用各自的更新机制,彼此互不干扰。

让生产机器一直可升级的几条习惯#

经过多年的运维经验积累,我总结出了一些避免踩坑的最佳实践。如果你不想在半夜被叫醒修服务器,可以参考以下建议:

1. 定期更新,而不是等到出问题才动手。 每周安排一次 apt upgrade -ydnf upgrade -y,并结合你的变更管理流程来执行。机器越久不更新,后续升级的复杂度和风险就越高。

2. 用完就清理,别留垃圾。

1
2
sudo apt autoremove -y && sudo apt autoclean      # Debian/Ubuntu
sudo dnf autoremove -y && sudo dnf clean all      # Red Hat 系列

如果发现 /var 突然满了,第一件事就是检查包管理器的缓存:

1
2
3
du -sh /var/cache/apt/archives    # Debian/Ubuntu
du -sh /var/cache/dnf             # Red Hat 系列
du -sh /var/lib/snapd/cache       # snap 的版本累积得很快

3. 关键组件要钉住版本: 像数据库引擎、加载了自定义模块的内核,或者你已经针对特定版本做过性能测试的软件,都可以用 apt-mark holddnf versionlock add 锁定版本,等到合适的时机再手动升级。

4. 同一个软件,要么用包管理器装,要么自己编译,别混着来。 如果你既用 apt install nginx 又用 make install 装了 Nginx,迟早会因为启动了错误的版本或修改了错误的配置文件而浪费时间排查问题。选一种方式坚持到底。

5. 自己编译的软件放对地方。 包管理器负责 /usr(除了 /usr/local),所以你自己编译的软件最好放在 /opt/usr/local 下,避免冲突。

6. 改配置文件之前一定要备份。

1
sudo cp /etc/nginx/nginx.conf{,.bak.$(date +%F)}

凌晨三点改崩了配置文件,包管理器可帮不了你,但你的备份可以。

7. 语言生态的东西用语言生态自己的隔离机制。 Python 项目可以用 python3 -m venvuv 创建虚拟环境, Node.js 的版本管理用 nvm,依赖管理用 npmpnpm。千万别用 pip install --user 把系统级 Python 环境搞乱了,因为其他系统工具可能还依赖它。

8. 执行前仔细看清楚计划。 aptdnf 在真正执行操作之前都会打印出完整的变更计划。如果发现它打算移除的包数量远超预期,立刻停下来,先搞清楚原因再继续。绝大多数「我用 apt 把系统搞崩了」的故事,都是因为有人在看到「即将删除 200 个包」的提示时直接按了 y

总结#

希望你能记住以下关于包管理的核心要点:

  • 软件包本质上是文件加上元数据,而包管理器的作用就是维护这些信息的一致性,并将其存储在一个能够跨重启持久化的数据库中。
  • 高级工具(如 aptdnfpacman)负责解决依赖关系并与软件仓库交互;低级工具(如 dpkgrpm)则专注于逐个处理单个文件。
  • 包管理中最棘手的部分是依赖解析,绝大多数问题都可以通过始终使用高级工具来避免。
  • 软件仓库是一棵经过签名的文件树,信任链从系统自带的 GPG 密钥开始,通过 Releaserepomd.xml 文件,最终传递到每一个 .deb.rpm 包。
  • 源码编译是当仓库中的版本不满足需求时的备选方案,但需要清楚自己承担了升级的成本。
  • Snap / Flatpak / AppImage 以牺牲磁盘空间和系统集成为代价,解决了「跨发行版分发」的问题。它们非常适合桌面应用,但在服务器领域用得较少。

扩展阅读:

系列下一篇:

  • 《Linux 进程与资源管理》—— cgroups、pstopnice、 OOM killer。
  • 《Linux 用户管理》—— 用户、组、 sudoers、 PAM。

当你能够熟练配置镜像源、锁定版本号、调试依赖冲突,并在「发行版包 / 源码编译 / Flatpak」之间根据实际需求做出合理选择时,你就真正掌握了包管理器,让它为你所用,而不是被它牵着鼻子走。

本系列

Linux 9 篇

  1. 01 Linux(一):使用基础
  2. 02 Linux(二):文件权限 —— rwx、chmod、chown 与超越它们的机制
  3. 03 Linux(三):磁盘管理
  4. 04 Linux(四):软件包管理 当前
  5. 05 Linux(五):用户管理
  6. 06 Linux(六):系统服务管理
  7. 07 Linux(七):进程与资源管理 —— 从 top 到 cgroups
  8. 08 Linux(八):文件操作深入解析
  9. 09 Linux(九):Vim 编辑器精要

读有所得?

GitHub 关注我 → 新文周更

GitHub