0 · 先建立正确的模型 dnf 在做什么,仓库的本质是什么
大多数人对包管理的理解停在「下载 + 安装」。那样记命令,配仓库时必然靠试。先把下面这条链记住,后面每一步都能自己解释。
| 步骤 | 它实际在做什么 | 失败时你会看到 | 对应的验证手段 |
|---|---|---|---|
| ① | 读 /etc/yum.repos.d/*.repo + /etc/dnf/dnf.conf | 仓库根本没出现 | dnf repolist --all |
| ② | 按 baseurl 取 repodata/repomd.xml | Cannot download repomd.xml | dnf makecache |
| ③ | 在元数据里算出「还要装哪些包」 | nothing provides ... | dnf provides */命令 |
| ④ | 下载 .rpm 到 /var/cache/dnf/ | Curl error / 超时 | 看 /var/log/dnf.log |
| ⑤ | GPG 校验、执行 rpm 事务 | GPG key ... NOT OK | rpm -qa | grep 包名 |
把 100 个 rpm 拷进
/opt/myrepo/,再写一个 baseurl=file:///opt/myrepo 的 .repo,dnf 一个包都看不见——因为那个目录里没有 repodata/。这就是 createrepo_c 存在的唯一理由:把 rpm 扫一遍,生成索引。$releasever:系统大版本号(Rocky 9 → 9),来自 system-release(releasever) 这个 provides。$basearch:CPU 架构(x86_64)。写成
.../rockylinux/$releasever/BaseOS/$basearch/os/ 就是为了「换机器不用改文件」。1 · 看清现状 先只读,一个字节都不要改
ls -l /etc/yum.repos.d/ dnf repolist dnf repolist --all
repolist 列出启用的仓库(id / name / status 三列);--all 多出 disabled 的行。.repo 文件里可以有多个 [section],每个方括号就是一个仓库。dnf repoinfo <id> 会告诉你它来自哪个文件第几行。dnf repoinfo baseos cat /etc/yum.repos.d/*.repo
Repo-baseurl(变量已展开成真实 URL)、Repo-filename、Repo-expire。dnf repolist 是会骗人的命令(你的老账本第 1 条):它读的是 /var/cache/dnf/ 里的本地缓存,不联网。网络断了、baseurl 已经 404,它照样给你列出两行漂亮的仓库。它只证明「配置读通了」,不证明「仓库可用」。dnf clean all dnf makecache
Metadata cache created.,每个启用的仓库一行,没有 error / Curl error。clean all 把本地缓存清空,makecache 被迫真的去 baseurl 拉一次 repodata。这是唯一能证明「仓库活着」的最小动作。Repo-expire 里的 21600 是哪来的:.repo 里写的是 metadata_expire=6h,dnf 内部换算成秒:6 × 3600 = 21600。配置文件里给人看,输出里给机器看——所以 grep 21600 在文件里一个都找不到。单位:
d 天 / h 小时 / m 分 / s 秒,不写单位默认按秒。想让元数据 1 小时过期就写 metadata_expire=1h。注意它不是「还剩多久过期」,而是「有效期长度」;后面括号里的
last: 才是上次刷新时间,两者相加才是下次过期时刻。不同仓库可以不同:rocky 系列是
6h,epel 是 14d(=1209600s),不写则用 dnf 默认值。dnf --disablerepo='*' --enablerepo=appstream list available | head dnf --disablerepo=appstream repolist
.repo 里的 enabled=,且只在这一次生效。2 · 配一个本地仓库(file://)本域最常考的一题,也是唯一需要「造」的动作
目标:把一个装满 rpm 的目录变成一个 dnf 能识别的仓库,并只从这个仓库装一个包。顺序不能颠倒:先有 rpm → 再生成 repodata → 最后才写 .repo。
# 路线 A:手上有 ISO / 安装光盘 mount /dev/sr0 /mnt ls /mnt/BaseOS/Packages | head # 路线 B:没有 ISO,从现有远程仓库下载几个包 mkdir -p /opt/localrepo dnf download --destdir=/opt/localrepo tree htop lsof
/opt/localrepo/ 里出现若干 .rpm(路线 B);或 /mnt/BaseOS/Packages/ 下有上百个 rpm(路线 A)。createrepo_c 要往目录里写 repodata/,光盘是只读的。所以要么把整个 Packages 拷到可写目录,要么把 ISO 挂载后 cp -r 出来。dnf install -y createrepo_c createrepo_c /opt/localrepo ls /opt/localrepo/repodata/
createrepo_c 打印 Working environment ... Saving Primary metadata ... 之类;repodata/ 里有 repomd.xml、primary.xml.gz、filelists.xml.gz。repomd.xml 就等于没做:dnf 只认这个文件。看到 repodata/ 目录存在但里面是空的,说明 createrepo_c 中途失败了,重跑一次看报错。createrepo_c --update /opt/localrepo,否则新包在索引里不存在。vi /etc/yum.repos.d/local.repo
[local]
name=Local Repo
baseurl=file:///opt/localrepo
enabled=1
gpgcheck=0
cat /etc/yum.repos.d/local.repo
baseurl 是 file:// + 三个斜杠(前两个是协议,第三个是根目录)。file:// 后面必须三个斜杠:file:///opt/localrepo 对,file://opt/localrepo 会被当成主机名 opt。gpgcheck=0 的取舍:自建仓库通常没有签名,关掉是最快的路;但生产上应导入 GPG key 并保持 gpgcheck=1。考试按题干要求写,没要求就 0。enable=1(少个 d),dnf 照样跑,因为 dnf 不校验未知键,而 enabled 的默认值就是 1,恰好蒙对了。危险的是反方向:想关掉却写
enable=0——这个键被忽略、默认仍是 1,仓库照样开着,屏幕上没有任何提示。所以验证只能看
dnf repoinfo local 里的 Repo-status: enabled / disabled,不能靠「命令没报错」。dnf clean all dnf --disablerepo='*' --enablerepo=local repolist dnf --disablerepo='*' --enablerepo=local list available
repolist 里只剩 local 一个仓库,status 是包数量而不是 0;list available 能列出你放进去的包名。repolist 会骗你:仓库能列出来,但 status 是 0,就说明 repodata 是空的或者包没进去。必须看到包名才算过。dnf --disablerepo='*' --enablerepo=local install -y tree rpm -qa | grep tree
Installing: tree-...,且 Repository: local;rpm -qa 能查到。为什么要 --disablerepo='*':不加的话,dnf 会优先从网络仓库拿——那样你根本证明不了本地仓库起作用了。把其它仓库都关掉,剩下的结果才归因明确。
already installed:说明这个包本来就在系统里(tree 很常见),dnf 不会重装,直接 Nothing to do。想练「真的从某个仓库装」,要么换一个没装的包,要么先 dnf remove 掉。另外
list available 只列未安装的包,所以已装的包在那里查不到,要看全部得用 dnf list --all。dnf --disablerepo='*' --enablerepo=local install htop 报 nothing provides libhwloc.so.15()(64bit) needed by htop。这不是本地仓库坏了,而是依赖求解只在「当前启用的仓库集合」里做——
hwloc-libs 在 appstream,被你一起关掉了,dnf 就找不到。所以这条命令的正确用途是证明「某个包确实来自本地仓库」,不适合用来装带依赖的包。要查依赖归属,得把仓库都开回来再问:
dnf provides '*/libhwloc.so.15'。3 · 加一个远程仓库 EPEL 是最常见的例子
# ① 启用一个已定义的仓库(这一步会真的改 rocky.repo) dnf config-manager --set-enabled crb grep -A6 '\[crb\]' /etc/yum.repos.d/rocky.repo # ② 关回去 dnf config-manager --set-disabled crb # ③ 从 URL 安装一个「仓库包」(epel 就是这么来的) dnf config-manager --add-repo https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
dnf repolist 多出 crb,且 grep 能看到 crb 段的 enabled=1 已被改写(这就是它与 --enablerepo 的根本区别);② 之后 crb 又消失;③ 之后 /etc/yum.repos.d/epel.repo 存在。dnf config-manager --enablerepo=x 已废弃,现在是 --set-enabled / --set-disabled。照着老教程敲会报 unknown option。--enablerepo = 本次临时生效;--set-enabled = 改配置文件、永久;--add-repo = 直接写出一个新的 .repo 文件。4 · 装 / 卸 / 回滚 把「装错了怎么办」也一起练
dnf install -y htop
rpm -qa | grep htop
dnf remove -y htop
rpm -qa | grep htop # 应该没有输出
dnf reinstall -y tree
Complete!;remove 后 rpm -qa 查不到;reinstall 会先 Reinstalling 再 Cleanup。remove 会连依赖一起删:卸载被别的包依赖的包时,dnf 提示要删一大串。看清楚列表再按 y,必要时用 dnf remove --noautoremove。卸载前问自己:这个包是系统自带的吗?删掉 base 组里的东西可能破坏环境。
repomd.xml(仓库的索引目录),确认「元数据变过了 / 本地没有」之后,才会去拉 44 MB 的 primary / filelists 全量。所以第一次或 dnf clean all 之后一定会看到两行;平时只看到第一行几 kB 才是正常的。install 一个已经在的包,dnf 输出 Package ... is already installed 加 Nothing to do,退出码仍是 0;remove 一个不存在的包,输出 package xxx is not installed 也不是失败。判断装没装上,只认 rpm -q 的输出,不认这两句话的语气。dnf history dnf history info last dnf history undo last
history 每行一个事务 ID;undo last 输出里把刚才装的东西再删回去,最后多出一条新的 history 记录(撤销本身也是一个事务)。dnf install -y 'dnf-command(versionlock)' dnf versionlock add tree dnf versionlock list dnf versionlock delete tree
list 里出现 tree-0:1.8.0-... 这样的行,带 0: 前缀和 epoch。'dnf-command(versionlock)' 是一种虚拟 provides——用「我需要的能力」而不是包名去装,这在不知道插件包叫什么时特别有用。同类的还有 dnf-command(config-manager)。dnf update 是「静默跳过」,不报错:命中时 dnf 直接把候选从求解过程里去掉,输出 Nothing to do. / Complete!,退出码 0。它不会被当成失败——所以别指望用「有没有报错」来判断锁生效了没有。用
grub2-tools(本机 126,仓库有 127)实测:·
dnf list --upgrades grub2-tools → 锁着也照样显示 127·
dnf update grub2-tools → Nothing to do.list / repoquery 走的是包集合查询,update / install 走的是事务求解器,versionlock 只塞进后者。⇒ 所以「锁前 / 锁后跑同一条 list」这个对照是无效的,别用。
dnf list --upgrades <包> 或 dnf check-update;② 看 dnf update <包> 是否真的不动。「有候选」+「不动」= 锁生效。dnf update <包> --assumeno 会自动答 No,只解析不执行,拿来复核很安全。--disableexcludes=all 绕不过 versionlock(实测):它只管 .repo 里的 excludepkgs,versionlock 的过滤走的是另一条通道。锁后执行 dnf update <包> --disableexcludes=all --assumeno,照样 Nothing to do.。只有 versionlock delete / clear 能解。① 显式写全 NVRA 也装不上:
dnf install 包-新版本 会报 All matches were filtered out by exclude filtering。锁不是「只挡自动选版本」。② 对没安装的包执行
add 是陷阱:会把当时仓库里所有可用版本全部加锁(实测 5 条),以后新版本永远进不来。动手前先 versionlock list 看清楚。/etc/dnf/plugins/versionlock.list,由 versionlock.conf 里的 locklist= 指定,dnf versionlock list 读的就是它。锁对 reinstall 无效 —— 锁的是当前版本,重装同一版本不算升级,照常执行。Nothing to do.。先用 dnf check-update 找一个真正有更新的包(本机当时是 grub2 系列 126→127)。dnf provides */semanage dnf provides /usr/bin/htop dnf provides '*/nc'
policycoreutils-python-utils-... : 提供 semanage 这类结果,带 Repo 和 Matched from。这是本域最该练熟的一条:考试说「安装提供 semanage 命令的包」时,你不需要背包名;真实运维里遇到 command not found,第一反应也应该是它,而不是去搜网页。
5 · 组与模块 RHEL 9 特有的一层
dnf group list dnf group list --installed dnf group info "Development Tools" # 只在这些情况下才真的装(本机未执行,Development Tools 约 100+ 包) # dnf group install -y "Development Tools"
group list 分「已安装组 / 可用组」两段;group info 列出 Mandatory / Optional 包清单。group list 输出的完全一致。写错会报 No group marked for installation。group list 的输出会分三段告诉你:Available / Installed Environment Groups —— 环境组,描述的是「整台机器装成什么样」,如
Minimal Install(本机已装的就是它)、Server with GUI。Available Groups —— 功能组,是「一堆包打包成一份清单」,如
Development Tools、System Tools。装环境组用
dnf group install,和功能组同一条命令;差别只在你选的是哪一类名字。dnf module list nodejs dnf module enable nodejs:18 dnf module install -y nodejs:18 dnf module list --installed dnf module reset nodejs
module list 里每个流有 [d](默认)/ [e](已启用)/ [i](已安装)标记;enable 之后状态变成 [e]。dnf module reset nodejs,否则报 Cannot enable ... conflicting requests。模块和普通包不要混装:模块管理的包用
dnf install 装,后续升级会被模块流限制。6 · 直接处理 rpm 文件 选做:不在考纲里,用来解释 dnf 的报错
dnf install /path/x.rpm;rpm -V / rpm2cpio 都不在考纲。这一章是「深层理解 + 排障」内容,可以先跳过,等 dnf 报错看不懂时再回来。rpm -q openssh-server rpm -qi openssh-server | head -12 rpm -ql openssh-server | wc -l rpm -qc openssh-server rpm -qd openssh-server rpm -qf /etc/ssh/sshd_config rpm -qR openssh-server | head -8 rpm -qpl /opt/localrepo/tree-1.8.0-10.el9.x86_64.rpm | head
·
-q → 一行 openssh-server-9.9p1-9.el9_8.rocky.0.1.x86_64·
-qi → Name / Version / Release / Install Date / Signature(签名行带 Key ID)·
-ql → 27 行·
-qc → 4 行(/etc/pam.d/sshd、/etc/ssh/sshd_config、/etc/ssh/sshd_config.d/50-redhat.conf、/etc/sysconfig/sshd)·
-qd → 4 个 man 页·
-qf → 反查回 openssh-server-9.9p1-...·
-qR → 依赖清单,/bin/sh 会重复出现多次(每个能力各要一次)·
-qp(加 -p)→ 查还没装的 rpm 文件①
-qc 出 4 行,rpm -ql | grep ^/etc 出 5 行 —— 少的那行是目录 /etc/ssh/sshd_config.d。-qc 只列文件级、且带 config 标记的条目,目录永远不列。②
-ql 含目录,-qc / -qd 不含。③
-qf 的路径必须真实存在;软链接要想到它可能归属另一个包。④ 一次查多个包时,只要有一个没装,退出码就是 3(各包状态仍逐条打印)。
rpm -q 单包 → 0 装了 / 1 没装;多包 → 3 表示有没装的。脚本里先 rpm -q pkg >/dev/null,别接管道(管道会把退出码换成后一个命令的)。rpm -V openssh-server echo "# lab" >> /etc/ssh/sshd_config rpm -V openssh-server chmod 644 /etc/ssh/sshd_config rpm -V openssh-server chmod 600 /etc/ssh/sshd_config
· 第一步 → 无输出,
echo $? = 0· 追加一行后 →
S.5....T. c /etc/ssh/sshd_config· 改成 644 后 →
.M.....T. c /etc/ssh/sshd_config· 改回 600 → 只剩
.......T. c /etc/ssh/sshd_configS Size 大小 · M Mode 权限 · 5 MD5 内容摘要 · D Device 设备号 · L Link 链接指向 · U User 属主 · G Group 属组 · T mTime 修改时间 · P caPabilities行尾的
c = config 文件,d = doc,什么都没写 = 普通文件。rpm -V 有输出 ≠ 文件坏了。改完再改回来,内容与权限都复原了,T 位照样亮 —— 因为 mtime 被永久改掉了,而 mtime 也是被校验的。判断「是否被篡改」要逐位看:只有 5(内容)位亮才是真的内容不对。mkdir -p /tmp/fix && cd /tmp/fix dnf download --destdir /tmp/fix openssh-server-9.9p1-9.el9_8.rocky.0.1 rpm2cpio openssh-server-9.9p1-9.el9_8.rocky.0.1.x86_64.rpm | cpio -idmv ./etc/ssh/sshd_config ls --full-time /tmp/fix/etc/ssh/sshd_config touch -r /tmp/fix/etc/ssh/sshd_config /etc/ssh/sshd_config rpm -V openssh-server
cpio 打印 ./etc/ssh/sshd_config 和 2992 blocks;ls --full-time 看到包内原始 mtime 2026-07-30 20:05:41;touch -r 之后 rpm -V 无输出、退出 0。①
dnf download openssh-server(不写版本)下到的是仓库最新版 13.el9,系统装的是 9.el9。版本不对,提取出来的是另一版配置 —— 必须写全 NVRA(名字-版本-发行版-架构)。②
cpio 的模式必须带 ./(包内路径就是 ./etc/...)。写成 etc/ssh/sshd_config 会一个文件都不提取,只打印 2992 blocks 然后退出 0 —— 看着像成功,实际什么都没做。③ 只复制内容不够:mtime 不同,
rpm -V 会一直报 .......T.。要 touch -r 借用包内文件的时间戳,别手写一个近似时间(20:05:00 是错的,真值是 20:05:41)。dnf reinstall openssh-server。rpm2cpio 是没有仓库、只能拿到一个 .rpm 文件时的兜底。rpm -ivh --test /opt/localrepo/tree-1.8.0-10.el9.x86_64.rpm rpm -ivh --test --replacepkgs /opt/localrepo/tree-1.8.0-10.el9.x86_64.rpm mkdir -p /tmp/t && dnf download --destdir /tmp/t httpd rpm -ivh --test /tmp/t/httpd-*.rpm rpm -ivh --test --nodeps /tmp/t/httpd-*.rpm dnf install -y /tmp/t/httpd-*.rpm
· 第 1 条 → 先打印
Verifying... Preparing... 两行进度条,最后一行才是 package tree-1.8.0-10.el9.x86_64 is already installed· 第 2 条 → 只有两行进度条,无报错、退出 0
· 第 4 条 →
Failed dependencies: httpd-core = 0:2.4.62-13.el9_8.9 is needed by ...; system-logos-httpd is needed by ...· 第 5 条加
--nodeps → 通过(因为 --test 不真装)· 最后
dnf install ./x.rpm → 自动把 httpd-core、system-logos-httpd 一起解出来装上--test 是唯一安全的试穿:依赖检查、文件冲突检查全跑一遍,一个文件都不写。所有不确定的 rpm 装卸,先加 --test。★ 进度条会骗人:
Verifying... Preparing... 出现不代表要装成了,它只是 rpm 在解包校验。只认最后一行和退出码。dnf = 仓库元数据 + 依赖求解;rpm = 单个文件的事务执行器,不联网、不解依赖。实践:能用
dnf install ./x.rpm 就别用 rpm -ivh;只有依赖确定齐全(或纯离线批量)时才用 rpm。-i 装(已存在则报冲突)· -U 升级或安装 · -F 只升级已装的 · --replacepkgs 允许重装同版本。rpm -e --test glibc 2>&1 | head -3 rpm -q --whatrequires openssh-server rpm -e --test openssh-server rpm -ivh --test /tmp/nonexist.rpm | head -2 echo $? rpm -ivh --test /tmp/nonexist.rpm > /tmp/e.txt 2>&1 echo $?
·
rpm -e --test glibc → 刷屏,3428 条 is needed by (installed) ...·
--whatrequires openssh-server → no package requires openssh-server·
rpm -e --test openssh-server → 无输出、退出 0·
| head 之后 echo $? → 0;改成重定向后 echo $? → 1① 没有输出 ≠ 没有依赖者。
openssh-server 确实没有包依赖它(--whatrequires 空),所以 rpm -e --test 干净退出。要判断"能不能卸",先问 --whatrequires。② sshd 正在运行 / 你正连着它,rpm 完全不看 —— rpm 只查 rpm 数据库里的依赖关系。
③
| head 会吞掉退出码(拿到的是 head 的 0)。脚本里要判成败,重定向到文件再 echo $?,这一条对所有命令都成立。--nodeps 是拆系统:rpm -e --nodeps 不管依赖硬删,删完系统可能"能开机但功能残缺"。真正需要时先用 --test 看清后果,再决定。7 · 附录一:命令速查 按「我想干什么」查
| 我想… | 命令 | 注意 |
|---|---|---|
| 看仓库列表 | dnf repolist --all | 读本地缓存,不联网 |
| 验证仓库可用 | dnf clean all && dnf makecache | 唯一算数的验收 |
| 临时关掉其它仓库 | --disablerepo='*' --enablerepo=X | 优先级高于 .repo |
| 找命令属于哪个包 | dnf provides */命令 | 不用背包名 |
| 只下载不安装 | dnf download --destdir=DIR 包名 | 需 dnf-plugins-core |
| 撤销上一次操作 | dnf history undo last | 是反向执行,不是删记录 |
| 锁版本 | dnf versionlock add 包 | 装 dnf-command(versionlock) |
| 装一个本地 rpm | dnf install ./x.rpm | 比 rpm -ivh 会解依赖 |
| 看文件属于哪个包 | rpm -qf /path | 比 dnf provides 更快 |
| 看包装了哪些文件 | rpm -ql 包名 | 配 -qc 只看配置 |
8 · 附录二:报错速查 看到这行字该想到什么
| 报错 | 真实原因 | 先查这个 |
|---|---|---|
Cannot download repomd.xml | 仓库地址不可达 / 路径写错 / DNS 不通 | curl -I <baseurl> |
Curl error 6: Could not resolve host | DNS 或网络 | ping / cat /etc/resolv.conf |
repodata/repomd.xml ... not found | 没跑 createrepo_c | ls 目录/repodata/ |
No package X available | 仓库里没有,或仓库没启用 | dnf --enablerepo=X list available |
nothing provides ... | 缺依赖包,仓库不全 | dnf provides */缺失的命令 |
GPG key retrieval failed | 没导入 key 但 gpgcheck=1 | rpm --import <keyurl> |
Failed dependencies(rpm 命令) | 用了 rpm -ivh,它不解依赖 | 改用 dnf install ./x.rpm |
Cannot enable ... conflicting requests | 模块流已被别的流占用 | dnf module reset X |
9 · 附录三:自测 先自己答,再展开
1. 一个装了 100 个 rpm 的目录,为什么 dnf 看不见里面任何包?
因为仓库的入口是 repodata/repomd.xml,不是 rpm 文件本身。没有索引,dnf 不知道目录里有什么、依赖关系如何。createrepo_c 就是生成这份索引的工具。
2. dnf repolist 显示两个仓库都很正常,为什么还可能装不上包?
因为它读的是本地缓存 /var/cache/dnf/,不联网。要证明仓库活着必须 dnf clean all && dnf makecache。「配置读通了」和「仓库可用」是两件事。
3. file:///opt/repo 和 file://opt/repo 差在哪?
前者是本地路径 /opt/repo;后者把 opt 当成主机名,会尝试走网络。本地路径必须三个斜杠。
4. 往本地仓库里新加了几个 rpm,需要做什么?
重新生成索引:createrepo_c --update /opt/localrepo,然后 dnf clean all。不重跑的话,新包在 repodata 里不存在,dnf 依然看不见。
5. 怎么只从一个仓库装包,而不改动任何配置文件?
dnf --disablerepo='*' --enablerepo=local install 包名。命令行参数优先级高于 .repo 的 enabled=,且只对本次生效。
6. 考试说「安装提供 semanage 命令的包」,你怎么找?
dnf provides */semanage。这比记包名(policycoreutils-python-utils)可靠得多,也是遇到 command not found 时的标准动作。
7. dnf install ./x.rpm 和 rpm -ivh ./x.rpm 的本质区别?
dnf 会连仓库、做依赖求解;rpm 只执行单个文件的事务,不解依赖、不联网。缺依赖时 rpm 直接 Failed dependencies。优先用 dnf 装本地包。
8. 装错了包,怎么回退?为什么说 undo 不是「时光倒流」?
dnf history undo last。它实际是再执行一次反向事务,不是删除历史记录;期间被手动改过的文件不会恢复。
9. 为什么 dnf module enable a:1 之后想换 a:2 会失败?
同一个模块同时只能启用一个流。要先 dnf module reset a 再 enable 另一个流。模块流是「整个系统只用这个版本」的承诺。
10. rpm -V 没有输出,说明什么?
说明该包所有文件的校验信息与安装时记录一致,没有被改动。没输出才是好结果——和 mount -a、parted 的成功行为一致。
11. BaseOS 和 AppStream 为什么要分成两个仓库?
BaseOS 放系统底座(内核、glibc、systemd 这类,只有一个版本),AppStream 放应用与多版本组件(可以用 module 选流)。分仓是为了让「底座稳定」和「应用可选版本」互不干扰。
12. 仓库文件里 enabled=0 的仓库,dnf install 会用吗?
不会,除非命令行临时 --enablerepo=那个仓库,或用 dnf config-manager --set-enabled 永久打开。