Linux 软件包管理
跨发行版的包管理实战:Debian/Ubuntu 的 dpkg/apt、RHEL/Rocky 的 rpm/dnf、Arch 的 pacman,依赖排障、版本锁定、镜像源、源码编译与 Snap/Flatpak/AppImage 等现代方案。
很多人是从「装、删、升」三个动词开始学包管理的,平时也够用,直到出问题——依赖冲突装不上、升级以后服务起不来、内核换完机器进不去系统、国内拉镜像慢得想哭。这时候你需要的不是再背几条命令,而是一个心智模型:一个包里到底装了什么、包管理器在背后到底求解什么、状态记录在哪里、Debian 系的 apt/dpkg 和 Red Hat 系的 dnf/rpm 在哪儿一致、又在哪儿分叉,凌晨两点登录线上机器才不至于慌。
这篇文章既给模型,也给配方。先讲一个 .deb 或 .rpm 文件里装了什么、为什么需要包管理器;再把 apt、dnf、pacman 摊开横向对比——不只是「等价命令表」,还包括它们在依赖求解、版本锁定、降级、仓库信任链上各自的脾气。然后是只能踩坑学到的事:把国内镜像源真正配对、源码编译 Nginx(顺便讲清楚 configure/make/make install 三步到底干了什么)、用二进制压缩包扔下一个 JDK、以及一组能让生产机器维持「永远可升级」状态的运维习惯。

一个包里到底有什么
软件包是一个文件(.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,你的文件原样不动。这条规则救过的线上事故比想象中多得多。
| |
3. 共享库。 即可执行文件运行时动态链接的 .so,相当于 Windows 的 DLL,放在 /usr/lib、/lib 或发行版特有的路径下,比如 /lib/x86_64-linux-gnu/。共享库的好处是省盘省内存(一份库被所有进程共用),更重要的是——升级一次 libssl3 就能把全机所有 TLS 客户端一起打补丁。
| |
4. 数据文件。 程序运行需要、但既不是代码也不是配置的东西:/var/lib/mysql 下的空数据库模板、/var/www/html 下的默认站点、locale 数据、示例证书等等。
5. 文档。 /usr/share/man/ 下的 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 -V 或 rpm -V 能告诉你「哪些文件被动过」的依据。
没有包管理器会怎样
得这样过日子:每装一个软件都要
- 自己找下载链接(顺便祈祷源站是干净的);
- 自己解依赖——A 要 B 1.1+,B 要 C,C 又跟你装过的 D 冲突;
- 自己决定每个文件放在哪;
- 自己记下你拷过哪些文件,以后才能卸干净;
- 自己定期上游网查安全更新。
包管理器把这堆活换成了一个数据库(/var/lib/dpkg/ 或 rpmdb)加一个求解器。数据库知道每一个已安装包占了哪些文件,求解器知道仓库里有什么、依赖怎么连,所有装、升、删都被规约成对这个数据库的一致变更。这就是包管理器的全部价值——而它的价值大得惊人。
主流的几个工具链
Linux 包管理几十年前就分成了若干派系,之后非常稳定。下面这张表是最该记的:
| 派系 | 代表发行版 | 包格式 | 底层工具 | 高层工具 |
|---|---|---|---|---|
| Debian | Debian / Ubuntu / Mint | .deb | dpkg | apt、apt-get |
| Red Hat | RHEL / CentOS / Rocky / Alma / Fedora | .rpm | rpm | dnf(EL7 上是 yum) |
| Arch | Arch / Manjaro / EndeavourOS | .pkg.tar.zst | pacman | pacman、yay |
| SUSE | openSUSE / SLES | .rpm | rpm | zypper |
| Gentoo | Gentoo | 源码 ebuild | - | portage(emerge) |
实战中真正要分清的不是派系,而是底层 vs 高层。底层工具(dpkg、rpm、裸 pacman -U)只对一个文件操作:解压、跑脚本、更新本地数据库——但不会主动去拉缺失依赖。高层工具(apt、dnf、pacman -S、zypper)是同样的动作外面套了一层依赖求解器和远程仓库通信。绝大多数时候你应该用高层工具;只有当你要做的事情正好是高层工具不让你做的——比如硬装一个理论上冲突的包,或者把一个 .deb 拆开看里面有什么——才轮到底层工具上场。
后面主要讲 apt(Debian/Ubuntu)、dnf(RHEL/Rocky/Fedora)和 pacman(Arch),中间穿插底层 dpkg / rpm 在哪些场景下要用。
依赖求解:这才是真正难的部分

包管理器最反直觉的地方是:它的代码大头不在「安装」,而在求解器。apt install nginx 实际要做的事情是:
- 从本地缓存的
Packages索引里读出 nginx 的元数据; - 遍历依赖图——
nginx依赖libssl3、libpcre2-8、zlib1g、libc6……每个又有自己的依赖,遇到「已安装且版本满足」就停; - 统一版本。如果两条路径要的
libc6版本不一样,求解器要么找一个能同时满足的版本,要么报冲突。共享库没有「两份都装」的选项——动态链接器按名字解析,不按路径,最终每个进程只会绑定到一份; - 决定安装顺序,让「必须先装的依赖」真的先装,这样安装后脚本运行时不会找不到东西;
- 找不到一致解就大声失败,绝不留下半装不装的烂摊子。
绕开高层工具,求解就没了:
| |
apt install -f 的作用就是事后把求解器跑一遍来修复部分安装的状态。其实你完全可以一开始就让高层工具来处理本地文件:
| |
卸载也一样。如果你装了 nginx 和它的 libcache-extra,单独卸 libcache-extra 没问题;但卸 nginx 时,原本只是为了 nginx 才装上的 libpcre2-8,如果没人再依赖它,应该顺手清理掉。apt autoremove 和 dnf autoremove 干的就是这件事。
Debian / Ubuntu:apt 和 dpkg
三个工具怎么选
| 工具 | 是什么 | 用在哪儿 |
|---|---|---|
apt | 现代用户友好前端:彩色输出、进度条、合理默认值。 | 交互式 shell、日常使用。 |
apt-get | 老前端,引擎相同,输出稳定、可被脚本解析。 | Shell 脚本、Ansible、Dockerfile。 |
dpkg | 底层工具,一次只处理一个 .deb,不解依赖。 | 检查包内容、强制状态、应急修复。 |
记忆方法:apt = apt-get + 交互优化;apt-get = 依赖求解器包着 dpkg;dpkg 才是真正解压文件、跑维护脚本的那一层。
日常命令
| |
新人最常踩的两条命令:
apt update不装任何东西,它只是把镜像源上最新的Packages索引拉到本地,下一条apt install才能看到最新版本。「明明仓库有这个包却装不上」十有八九是没跑过update。apt remove保留/etc。绝大多数场景这正是你要的(重装时还能恢复配置);但如果你是真要彻底下线一个服务,应该用apt purge。
搜索、查看、定位
| |
后两条是排障神器。「这个包到底往磁盘上扔了什么文件?」「PATH 里这个不知道哪来的二进制是哪个包带的?」是事故调查的高频问题,而 Linux 上没有别的命令能像 dpkg -S 这样反查文件归属。
锁定、降级、冻结
| |
hold 是你应对「上游发了一个有问题的版本」的武器:钉住老版本,找个维护窗口再升级,不让自动化在半夜替你做决定。
清理
| |
我自己肌肉记忆里常用的一条组合:
| |
每周对每台机器跑一次,/var 不会平白被撑爆。
RHEL / Rocky / Fedora:dnf(顺带 yum 和 rpm)
dnf 在 CentOS 8 / RHEL 8 和 Fedora 22 替代了 yum,命令名几乎一一对应(这是有意保留的兼容性),但底层用 Python 重写,并换上了真正的 SAT 求解器(libsolv)。所以 dnf 在大事务上明显更快。CentOS 7 还在用 yum,其他地方都按 dnf 来。
日常命令
| |
dnf downgrade 是 dnf 比 apt 体验好的地方之一——apt 世界里降级是「装回老版本祈祷不冲突」,dnf 里它是一等公民。
搜索、查看、定位
| |
rpm -V 是被低估的工具。输出 S.5....T. c /etc/nginx/nginx.conf 的解读是:大小变了(S)、MD5 变了(5)、修改时间变了(T),且这是一个配置文件(c)。配置文件被改是正常的;但如果 /usr/sbin/ 下的二进制被改了——这就是排查入侵痕迹的入口。
锁定与历史
| |
dnf history 没有真正的 apt 等价物。它把每一次事务的时间戳、命令行、完整 diff 都记下来,并且任何一次都能回滚。在被多任管理员折腾过的服务器上,这是你还原「到底什么时候发生了什么」的唯一办法。
Arch:pacman
Arch 的 pacman 既是底层也是高层工具。Arch 是滚动发行,不存在「Arch 22.04」,只有「现在仓库长什么样」,所以工作流天然偏向「永远整体升级」:
| |
Arch 的铁律:永远不要部分升级。隔几周没跑 pacman -Syu 直接来一句 pacman -S nginx,你装的可能是依赖了新版本库的 nginx,而你机器上还是老库——结果就是 nginx 起不来。要么先 -Syu,要么干脆 pacman -Syu nginx 一步到位。
Arch User Repository(AUR)由社区维护构建脚本,前端工具如 yay、paru 把 pacman 加上 makepkg 一锅端:拉源码、构建、安装。挺好用,但 AUR 的东西要按「我会读完 PKGBUILD 再装」的态度来对待。
一台机器上的包生命周期

抛开具体工具,每个包在你机器上都走同样的循环:搜 → 装 → 升(或冻结)→ 卸。包管理器把每个包的当前状态记在数据库里(Debian 是 /var/lib/dpkg/status,Red Hat 是 /var/lib/rpm/ 下的 rpmdb)。你跑的每一条命令,本质上都是对这个数据库的一笔事务,加上对磁盘的对应操作。
正因如此,「我手动 rm 掉那个二进制了」永远是错的:数据库里那个包还显示装着,下次升级会悄悄把文件放回来,dpkg -V / rpm -V 又会报「文件丢失」。要么把包正经卸掉(apt purge / dnf remove),要么——如果你确实需要保留包但屏蔽某个文件——用 dpkg-divert 这种正经手段。
仓库到底长什么样

apt update 时,机器走 HTTPS 连镜像,按一套很具体的目录结构去拉索引文件。理解这个结构,调试镜像问题就只是 curl 几条 URL 的事。
Debian 风格的 apt 仓库提供两棵树:
dists/<suite>/:每个 suite 的元数据。顶层文件是Release(或者它的 inline-签名版本InRelease),里面列出每个组件(main、universe等)和架构,附上每个Packages.gz索引的 SHA256。Release.gpg是分离的 GPG 签名。这棵树是信任的根。pool/:真正的.deb文件,按源码包名分目录(pool/main/n/nginx/nginx_1.24.0-1_amd64.deb)。
apt install nginx 的信任链是:
- 你机器上
/etc/apt/trusted.gpg.d/里有一组你信任的 GPG 公钥(通过源里的signed-by=字段或老的apt-key放进去); apt update拉InRelease,用上面那些公钥验签——验不过就直接拒绝使用整个 suite(NO_PUBKEY错误就来自这一步);- 从
InRelease拿到每个组件Packages.gz的 SHA256,拉下来再校验; - 从
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):
| |
常见 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):
| |
清华源把上面命令里的域名换成 mirrors.tuna.tsinghua.edu.cn 即可,路径完全一样,按你网络情况选快的那个。配完一定要真的跑一次升级(apt upgrade -y / dnf upgrade -y),有问题立刻就能发现,省得真要装东西时才意识到镜像根本没生效。
源码编译:configure / make / make install
有时候仓库里的版本太老,或者你需要一个发行版包没有的编译选项(一个 Nginx 模块、一个特定 OpenSSL 版本、一个调优开关)。源码编译就是后路。经典的 autotools 三步曲:
./configure:探测系统——什么编译器、有哪些库、有哪些头文件,再根据你给的参数生成Makefile;make:按这个Makefile调编译器把代码编出来;sudo make install:把编译产物拷到Makefile里记录的目录(通常就是你给configure的--prefix)。
CMake 和 Meson 的命令不一样(cmake -B build / cmake --build build / cmake --install build),但形状一致:先 configure,再 build,再 install。
实战:在 Ubuntu 上编译 Tengine
Tengine 是淘宝维护的 Nginx 分支,多了不少模块。构建流程跟上游 Nginx 完全一致——会编 Tengine 就会编 Nginx。
| |
每一步实际干了什么:
./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 单元、建用户、开防火墙——这些都得你自己来。
跑起来:
| |
接到 systemd 下管的话,写个 /etc/systemd/system/nginx.service:
| |
然后:
| |
源码编译要付的代价是:没有 apt upgrade 替你管它。升级路径、安全跟踪、OpenSSL 出问题时重编整个软件,都是你自己的工作。大多数生产场景里正确答案是「除非有具体理由,否则用发行版包」。源码编译是用来解决那个「具体理由」的。
二进制压缩包:以 JDK 为例
有些软件以「解压即用」的二进制压缩包发布——JDK、Go、Node.js、大部分数据库引擎都有这种形式。没有安装程序好商量,文件放哪、环境变量怎么设,都是你的决定。
| |
/etc/profile.d/*.sh 会被所有用户的登录 shell source 到,这正是 JDK 想要的作用域。只想给当前用户用,写到 ~/.bashrc 或 ~/.zshrc 即可。要并行多个 JDK,统一放在 /opt/jdk-XX 下,靠切 JAVA_HOME 切换(或者上 sdkman、jenv)。
同样的套路适用于 Go(/opt/go)、Node.js(/opt/node-vXX)等等。它们之所以流行,正是因为绕开了发行版的发布周期——上游版本和你急用的版本不匹配时这一招特别管用。
现代方案:Snap、Flatpak、AppImage

发行版包模型有个长期痛点:它把软件牢牢绑在发行版的其它部分上。一个针对 Ubuntu 22.04 的 glibc 和 gtk 编出来的 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:单个自包含可执行文件,不装、无守护进程、默认无沙箱。双击就跑,「我只想试一下这个软件」或者内部工具分发的好选择。
| |
什么时候用哪个:
- 服务器端的东西(
nginx、mysql、python、redis)一律走发行版包:体积小、跟 systemd 集成好、安全更新由发行版安全团队推送。 - 桌面 GUI 应用且发行版包跟不上上游的:上 Flatpak(Flathub 选择最广)或 Snap。
- 临时跑一下试试的工具:AppImage。
四种可以同台共存,安装根目录、更新机制都是分开的,互不干扰。
让生产机器一直可升级的几条习惯
这是我修过太多别人留下的烂摊子之后总结出来的:
1. 按计划升级,不是出事再升。 每周一次 apt upgrade -y / dnf upgrade -y,走你那套变更管理流程。一台机器越久没升级,下一次升级就越疼。
2. 边用边清。
| |
/var 莫名其妙满了,第一时间查包缓存:
| |
3. 升不起的就钉死。 数据库、带自定义模块的内核、被你压测过特定版本的中间件——apt-mark hold / dnf versionlock add 锁住,留到运维窗口里手动升。
4. 包管理器和源码编译的同一软件不要并存。 apt install nginx 和 make install 都往 PATH 里塞了 nginx,迟早你会启错那个、改错配置,然后浪费一小时排查。只用其中一种。
5. 源码装的东西放 /opt 或 /usr/local。 包管理器拥有 /usr(除了 /usr/local),你别去抢它的地盘。
6. 改 /etc 之前先备份。
| |
凌晨三点你把配置改坏了,包管理器救不了你,备份能。
7. 语言生态用语言生态自己的隔离。 Python 用 python3 -m venv 或 uv,Node.js 版本用 nvm、项目依赖用 npm/pnpm。别让 pip install --user 污染了系统 Python——其它发行版包还指望它呢。
8. 看清楚你在做什么。 apt/dnf 在执行前会把整个计划打印出来。如果它要删的包数量明显异常,先停下来搞清楚原因再确认。「我用 apt 把系统弄废了」的故事,绝大多数都开头于:有人在「即将删除 200 个包」的提示里直接按了 y。
总结
希望你带走的心智地图:
- 包是文件加元数据;包管理器存在是为了让这个映射在数据库里始终一致并跨重启保留。
- 高层工具(
apt、dnf、pacman)跑求解器、对接仓库;底层工具(dpkg、rpm)一次只处理一个文件。 - 包管理器真正难的地方是依赖求解,绕开高层工具基本就是给自己找事。
- 仓库是签名过的文件树,信任从你机器上的 GPG 公钥,沿
Release/repomd.xml一路传到每一个.deb/.rpm。 - 源码编译是仓库版本不顶用时的后路;自愿背上升级税。
- Snap / Flatpak / AppImage 用磁盘和集成度换「跨发行版分发」,桌面应用合适,服务端少见。
扩展阅读:
- Debian 包管理参考:https://www.debian.org/doc/manuals/debian-reference/ch02
- DNF 文档:https://dnf.readthedocs.io/
- Arch Pacman/Rosetta:https://wiki.archlinux.org/title/Pacman/Rosetta (跨发行版命令对照表)
- RPM 打包指南:https://rpm-packaging-guide.github.io/
系列下一篇:
- 《Linux 进程与资源管理》—— cgroups、
ps、top、nice、OOM killer。 - 《Linux 用户管理》—— 用户、组、sudoers、PAM。
当你能配镜像源、能锁版本、能调试依赖冲突、能在「发行版包 / 源码编译 / Flatpak」三者之间按理由选择时,你就从「跟包管理器较劲」升级到「让它替你干活」了。