T7、RHEL alternatives

作者: Brinnatt 分类: 小工具 发布时间: 2025-10-09 11:55

文档版本:2.0
适用对象:系统管理员、运维工程师、配置管理(Ansible/Puppet/Chef)、包维护者
参考来源:Debian man update-alternatives(1)、Debian Policy、Fedora Wiki、RHEL 文档、SUSE 管理指南


1. 文档说明与快速定位

需求 跳转章节
快速会用 2. 快速入门
本质与边界(为何存在、解决什么、不解决什么) 3. 本质与要解决的问题
设计(两层链接、link group、auto/manual、与包管理契约) 4. 设计动机与约束
实现与机制(状态存储、各操作触发的行为、包脚本契约) 5. 实现与机制
术语与概念 6. 术语与概念
发行版差异 7. 发行版与路径差异
命令语法 8. 命令参考13. 命令速查
深入实践案例(Java 全族、MTA、自建、Python、批量、故障) 10. 实践案例
排错与最佳实践 11. 排错与常见问题12. 最佳实践

2. 快速入门

  • 一句话:Alternatives 是系统级的符号链接管理框架,让多个“可互换”实现(如多版本 Java、多种编辑器)并存时,由一个通用命令名通过符号链接指向当前选中的实现,并由优先级或管理员选择决定指向谁。
  • 最常用(Red Hat 系,Debian 系把 alternatives 换成 update-alternatives):
    • 查看:alternatives --display java
    • 交互切换:sudo alternatives --config java
    • 脚本固定:sudo alternatives --set java /path/to/java-17/bin/java
  • 链接链/usr/bin/java/etc/alternatives/java → 实际二进制;切换时只改中间一层。

3. 本质与要解决的问题

3.1 问题背景

  • 多个包可提供同一“逻辑接口”的不同实现(如多种 vi、多种 Java、多种 MTA),且允许同时安装
  • 系统与脚本需要一个稳定的通用名(如 vijavaeditor)来调用“当前默认”的实现,而不必写死路径或版本号。
  • 若没有统一机制:要么每个包抢占同一路径(互相覆盖),要么由管理员手写符号链接,包升级或安装/卸载会破坏或覆盖这些链接,且无法区分“包提供的候选”与“管理员的选择”。

3.2 本质定义

Alternatives 的本质是:在“多实现并存”的前提下,用“状态 + 符号链接”维护“当前默认实现”的单一真相源,并约定包管理与管理员谁有权在何时改写该状态。

  • 状态:每个链接组有 mode(auto/manual)current selection(或“由优先级推导的 best”);状态存放在 administrative directory,与 /etc/alternatives 的符号链接一起构成完整事实。
  • 符号链接:通用名(如 /usr/bin/java)不直接指向具体实现,而是指向 /etc/alternatives/<name>,再由后者指向真实路径。这样“谁被选为默认”的变更只体现在 /etc 下的链接,符合 FHS 对“管理员可改配置”的归属。
  • 契约:包在 postinst(configure) 注册、在 prerm/postrm(remove) 注销;不在 upgrade/disappear 等阶段调用 alternatives,以免破坏 manual 或造成反复切换。

因此,理解 alternatives 的核心是:谁在什么时机可以改“当前默认”和“模式”,以及链接如何与状态一致。

3.3 边界:什么不用 alternatives

场景 更合适的做法 说明
用户级“默认”(如 A 用 vim、B 用 nano) 环境变量 EDITORVISUAL 或 shell 配置 alternatives 是系统级;per-user 用 env 或 pyenv/jenv/nvm 等
编译时选编译器 环境变量 CCCXX 或构建系统配置 不要用 alternatives 代替环境变量
替换某包提供的唯一文件(无多实现) diversions(dpkg 的 divert) alternatives 用于“多选一”,diversions 用于“替换单文件”
systemd 配置等 不用 alternatives Debian Policy 明确不用于 systemd 配置文件

4. 设计动机与约束

4.1 为什么是“两层”符号链接(generic → alternatives 目录 → real)

  • FHS/usr 属于“不变的系统文件”,/etc 属于“主机相关、管理员可改”的配置。若把 /usr/bin/java 直接链到 /usr/lib/jvm/...,则“当前默认”的语义会写入 /usr,与 FHS 相悖;且包管理在安装/卸载时若改写 /usr/bin/java,容易与管理员意愿冲突。
  • 设计选择:让 /usr/bin/java 只指向 /etc/alternatives/java,而“当前选中的实现”由 /etc/alternatives/java 再指到具体路径。这样:
    • 包可以只安装自己的二进制到 /usr/opt,并注册到 alternatives;
    • 真正“改默认”的操作只改 /etc/alternatives 下的链接(及状态),管理员改动被限定在 /etc,符合 FHS,且包升级不直接覆盖“谁被选中”。

(出处:Debian man — “so that the system administrator's changes can be confined within the /etc directory”.)

4.2 为什么需要 link group(master + slave)

  • 一个“实现”往往不止一个文件:例如选 JDK 17,则 javajavackeytool、以及对应 man 页都应一起指向 17,否则会出现 java 是 17 而 javac 仍是 11 的不一致。
  • link group:由一个 master link(如 java)和若干 slave links(如 javacjava.1.gz)组成;切换“当前实现”时,只改 master 的选择,所有 slave 随 master 一起切换,保证一组链接始终对应同一实现。

4.3 为什么有 auto 与 manual 两种模式

  • auto:包安装/卸载时,由系统根据优先级自动决定“当前默认”(通常选最高优先级且仍存在的实现)。适合“随包进退、自动选最优”的场景。
  • manual:当前指向由管理员显式选择--config--set);包安装/卸载不会改变该组的指向。适合生产上“固定版本、禁止包升级悄悄换默认”的场景。
  • 一旦管理员通过 --config / --set 做出选择,该组会变为 manual,以尊重“人”的决策;若要恢复“自动选最高优先级”,需显式执行 --auto <name>

4.4 优先级的角色

  • 每个候选有一个整数 priority数值越大越优先
  • 仅在 auto 模式下:系统用 priority 决定“当前默认”应是哪一个(通常为已安装中 priority 最高者);manual 模式下 priority 仅作展示或 --display 中的“best”参考,不驱动切换。

4.5 与包管理的契约(Debian Policy 与 man 的硬性约定)

  • 应当调用
    • postinst (configure):注册本包提供的 alternative(--install);
    • prerm (remove):在删除文件前从 alternatives 中移除本包提供的 path(--remove name path);
    • postrm (remove):也可参与移除,但若在文件已删后调用 --remove,该 path 已不存在,可能被标为“站点本地”等,故推荐在 prerm 中做 --remove
  • 不应调用(否则易丢 manual 或反复切换):
    • upgradedisappear 等阶段不要调用 update-alternatives 来改链接;仅在 configure/remove 时注册/注销候选即可。
  • 维护脚本中避免 --set:在包脚本里用 --set 会把链接组切成 manual,无法区分“用户选的”和“包强制的”,且会阻止后续自动管理(Debian Bug #643602)。

5. 实现与机制

5.1 目录布局与职责

路径 职责
/usr/bin/(或其它 generic name) 用户可见的入口;必须是指向 /etc/alternatives/<name> 的符号链接(由 alternatives 创建/更新)。
/etc/alternatives/ 由 alternatives 管理的“中间层”符号链接;指向当前选中的实现(真实二进制或文件)。
/var/lib/alternatives/(RHEL)或 /var/lib/dpkg/alternatives/(Debian) 状态库:每个链接组对应一个文件(通常与 <name> 同名),记录 mode、master/slave 的 generic 路径、所有候选的 path+priority+slave 列表、以及当前选中 path(manual 时)或“由 priority 推导”的 best。

注意:不要手动用 ln -sf/etc/alternatives 或 generic name;所有改动必须通过 --set / --config / --install / --remove 等,否则状态与链接不一致,后续行为不可预期。

5.2 状态存储(概念化,实现可略有差异)

每个链接组在 administrative directory 下有一个状态文件(名称即 <name>)。其逻辑内容大致包括:

  • 模式:auto 或 manual。
  • master:generic 路径(如 /usr/bin/java)、以及“当前指向的 path”(manual 时显式存;auto 时可为“best”或等价信息)。
  • 候选列表:每个候选一条记录 —— path、priority、以及该候选下各 slave 的 (slave generic path, slave name, slave path)。
  • slave 列表:与 master 同组的各 slave 的 generic 路径与名称,用于在切换时一起更新。

(具体磁盘格式各实现不同,此处仅说明“状态里有什么”;运维只需通过 --display--get-selections 等观察,勿直接编辑状态文件。)

5.3 各操作触发的行为(机制要点)

操作 对状态的影响 对链接的影响
--install <name> 不存在则新建链接组(mode=auto);将 (path, priority, slaves) 加入候选。若已存在则仅新增/更新该候选。 若 mode=auto 且新候选 priority 为当前最高,则把 master 与所有 slave 的链接更新为指向该候选;否则若为新建组,则指向该唯一候选。可能创建 generic name → /etc/alternatives/ 的链接(若尚未存在)。
--remove name path 从该组的候选列表中删除该 path(及对应 slave 信息)。 当前指向的就是该 path,则把当前指向改为其他候选(或删链);并可能把组改回 auto。否则只删记录,不改链接。
--set name path 将“当前选中”设为 path,并将组设为 manual 立即把 /etc/alternatives/<name> 及该组所有 slave 的链接更新为 path 对应的目标。
--config name 用户选择后,将当前选中设为所选 path,并将组设为 manual 同 --set,按选择更新所有链接。
--auto name 将组设为 auto;当前选中逻辑改为“best”(最高 priority 且存在)。 将 master 与 slave 链接更新为指向 best。
--remove-all name 删除该链接组的全部状态与候选。 删除 /etc/alternatives/<name> 及该组涉及的 generic 链接(视实现而定)。

要点:

  • auto 下:每次执行 --install / --remove 时,若当前选中的实现被删或出现更高 priority 的候选,链接会随之更新。
  • manual 下--install / --remove 不改变当前指向,只增删候选记录;只有 --set / --config / --auto 会改链接。

5.4 包维护脚本何时调用(小结)

  • postinst configure--install 注册本包提供的 alternative。
  • prerm remove--remove <name> <path> 在文件被删之前注销,避免状态仍指向已删 path。
  • 不要在 upgrade、disappear 等阶段调用以“切换”或“重选”链接,否则会丢失 manual 或产生 flip-flop(Debian man 明确警告)。

6. 术语与概念

英文术语 含义
generic name / alternative link 用户调用的路径,如 /usr/bin/editor/usr/bin/java
alternative name 链接组名,即 /etc/alternatives/<name> 中的 name,如 editorjava
alternative (path) 某个具体实现路径,如 /usr/bin/vim/opt/jdk-17/bin/java
alternatives directory 默认 /etc/alternatives,存“中间层”符号链接
administrative directory RHEL:/var/lib/alternatives;Debian:/var/lib/dpkg/alternatives,存状态
link group 一个 master + 多个 slave,一起切换
master link 决定整组指向哪个实现的主链接
slave link 随 master 同步切换的从属链接

7. 发行版与路径差异

项目 RHEL / CentOS / Fedora Debian / Ubuntu
命令名 alternatives update-alternatives
可执行路径 /usr/sbin/alternatives 随 dpkg
状态目录 /var/lib/alternatives /var/lib/dpkg/alternatives
中间层链接 /etc/alternatives /etc/alternatives
提供包 常为 chkconfig 包 dpkg 包
扩展 --initscript(chkconfig) --get-selections / --set-selections

8. 命令参考

8.1 --install

alternatives --install <link> <name> <path> <priority> [--slave <link> <name> <path>]...
  • <link>:主链接的通用路径(如 /usr/bin/java),必须绝对路径
  • <name>:链接组名(如 java)。
  • <path>:本候选的真实路径,必须绝对路径
  • <priority>:整数,越大在 auto 下越优先。
  • --slave link name path:从属链接的 (用户路径, 组内名, 实际路径),可多组。

8.2 --remove / --remove-all

alternatives --remove <name> <path>
alternatives --remove-all <name>

8.3 切换与模式

alternatives --config <name>    # 交互,选后设为 manual
alternatives --set <name> <path>   # 非交互,设为 manual
alternatives --auto <name>     # 恢复 auto,指向最高 priority

8.4 查询

alternatives --list
alternatives --display <name>

Debian 额外:--get-selections--set-selections--list <name>(列出该组所有候选 path)。

8.5 可选参数

  • --altdir--admindir:覆盖默认的 alternatives 目录与状态目录。
  • --verbose / --quiet

退出码:0 成功,2 错误(与 man 一致)。


9. 实践案例(概要)

  • Java 多版本:用 --install 注册多个 JDK,并用 --slave 绑定 javackeytool、man 等;生产用 --set 固定版本。
  • 默认编辑器alternatives --config editor(或 update-alternatives)。
  • MTA(RHEL):sendmail 与 postfix 通过 mta 链接组切换。
  • 自建多版本:如 /usr/local/bin/foo 在 foo-2 与 foo-3 间切换,并带 slave(man、config);先 --install 两个候选再 --config--set
  • RHEL 8 无版本号 python:用 alternatives --set python /usr/bin/python3.11 提供 /usr/bin/python;注意不要为系统自带的 python3 乱建 alternative(SUSE 文档警告:不要为 python3 做自定义 alternative,会破坏依赖)。
  • 批量部署(Debian)--get-selections 导出,--set-selections 在它机或重装后导入。

10. 实践案例(深入)

10.1 Java 完整链接组(master + 多个 slave)

目标:javajavackeytool、man 等随同一 JDK 一起切换,且生产固定版本。

# 注册 JDK 11
alternatives --install /usr/bin/java java /usr/lib/jvm/java-11-openjdk/bin/java 1100 \
  --slave /usr/bin/javac javac /usr/lib/jvm/java-11-openjdk/bin/javac \
  --slave /usr/share/man/man1/java.1.gz java.1.gz /usr/share/man/man1/java-11.1.gz

# 注册 JDK 17(priority 更高,auto 下会成默认)
alternatives --install /usr/bin/java java /usr/lib/jvm/java-17-openjdk/bin/java 1700 \
  --slave /usr/bin/javac javac /usr/lib/jvm/java-17-openjdk/bin/javac \
  --slave /usr/share/man/man1/java.1.gz java.1.gz /usr/share/man/man1/java-17.1.gz

# 生产固定为 17
alternatives --set java /usr/lib/jvm/java-17-openjdk/bin/java

理解要点:每个 --install 的 slave 列表必须与该候选的 path 一一对应;切换时整组一起变,避免 java 与 javac 版本不一致。

10.2 MTA 组(sendmail vs postfix)

RHEL/Fedora 上常见“mta”链接组:/usr/sbin/sendmail 等由 alternatives 指向 sendmail 或 postfix 的实现。切换方式同上:alternatives --config mta--set mta <path>。理解:同一“逻辑接口”(MTA)的多实现,由一组链接统一切换。

10.3 自建多版本命令(带 man、config 的 slave)

场景:自建脚本 foo-2foo-3,希望 /usr/local/bin/foo 可切换,且 man、配置文件随版本一起换。

# 两套实现:/usr/local/bin/foo-{2,3},/usr/local/man/man1/foo-{2,3}.1.gz,/etc/foo-{2,3}.conf
sudo alternatives --install /usr/local/bin/foo foo /usr/local/bin/foo-2 200 \
  --slave /usr/local/man/man1/foo.1.gz foo.1.gz /usr/local/man/man1/foo-2.1.gz \
  --slave /etc/foo.conf foo.conf /etc/foo-2.conf

sudo alternatives --install /usr/local/bin/foo foo /usr/local/bin/foo-3 300 \
  --slave /usr/local/man/man1/foo.1.gz foo.1.gz /usr/local/man/man1/foo-3.1.gz \
  --slave /etc/foo.conf foo.conf /etc/foo-3.conf

sudo alternatives --config foo   # 或 --set foo /usr/local/bin/foo-3

要点:slave 的 generic 路径(如 /usr/local/man/man1/foo.1.gz)在两组中相同,但各自指向不同文件,保证 man foo 与当前 foo 版本一致。

10.4 RHEL 8 的 /usr/bin/python(无版本号)

RHEL 8 不默认提供未版本化的 python;若需要,用 alternatives 指向某个 python3.x:

sudo alternatives --set python /usr/bin/python3.11

注意:不要为系统自带的 /usr/bin/python3 再建一个“覆盖型”的 alternative(某些文档警告会破坏依赖);仅当发行版明确支持“python”这个 generic 名时再设。

10.5 批量部署与配置管理(Debian)

# 导出当前所有选择(备份或做黄金镜像)
update-alternatives --get-selections > alternatives-selections.txt

# 在新机或重装后批量恢复
sudo update-alternatives --set-selections < alternatives-selections.txt

在 Ansible 等工具中:可把 --get-selections 输出作为“事实”或模板,用 --set-selections 做幂等配置;生产建议对关键组(如 java)再用 --set 明确一次,防止后续包操作影响。

10.6 故障根因与对应手段

现象 本质原因 做法
切换后命令行版本没变 实际执行的是 PATH 里别的路径或 shell alias readlink -f $(which java) 看真实链;检查 alias$PATH
包升级后默认被改 组处于 auto,新包 priority 更高 生产用 --set 固定(manual)
不同用户看到不同默认 用户层 PATH/alias 不同 alternatives 是系统级;查 ~/.profile~/.bashrc/etc/profile.d
删除某版本后报错或断链 状态仍指向已删 path --remove <name> <path> 从候选中移除该 path
手动改过 /etc/alternatives 或 ln -sf 状态与链接不一致 仅用 --set/--config 改;若已乱,可用 --install 重新注册再 --set 纠正
包升级后“默认”来回变 维护脚本在 upgrade/disappear 里调用了 alternatives 包侧应在 postinst configure / prerm remove 才调用;本地只能 manual 固定

11. 排错与常见问题

(与上节 10.6 一致,此处仅列命令级排查。)

  • 看链接链:ls -l /usr/bin/java /etc/alternatives/javareadlink -f /usr/bin/java
  • 看组状态与候选:alternatives --display java(或 update-alternatives --display java)。
  • 确认“当前”与模式:--display 首行会标 auto/manual 及当前指向。

12. 最佳实践

  • 生产固定版本:对关键接口(如 java、python)用 --set 设为 manual,避免包升级自动换默认。
  • 脚本化与幂等:把 --install--set--set-selections 纳入 Ansible/Puppet/Chef,保证可重复执行。
  • 成组一致:Java、GCC 等一族命令用 --slave 绑定,避免 java/javac 或 gcc/g++ 版本不一致。
  • 巡检与审计:定期 alternatives --display <name>--get-selections 留存结果,便于比对变更。
  • 区分用途:alternatives 管系统级默认;per-user 用环境变量或 pyenv/jenv/nvm 等。
  • 环境变量EDITORCC 等用环境变量或配置设置,不用 alternatives 代替。

13. 命令速查

操作 命令
列出所有组 alternatives --list
某组详情 alternatives --display <name>
交互切换 alternatives --config <name>
非交互指定 alternatives --set <name> <path>
恢复自动 alternatives --auto <name>
注册 alternatives --install <link> <name> <path> <priority> [--slave ...]
移除候选 alternatives --remove <name> <path>
移除整组 alternatives --remove-all <name>
批量导出/导入(Debian) update-alternatives --get-selections / --set-selections

14. 常见误区与纠正

错误 正确
--install 用相对路径 所有 link、path 必须绝对路径
只注册 java 不配 javac 等 slave --slave 成组注册,保证一致
手改 /etc/alternatives 或 ln -sf 仅用 --set / --config 修改
新候选 priority 低于现有却期望成默认 auto 下只有 priority 最高者会被选;要么提高 priority,要么用 --set

15. 参考与引用


修订时请更新文首版本号;行为描述以各发行版 man 与 Policy 为准。

标签云