
前两天帮同事处理一台内网编译服务器系统是 CentOS 8。他要装 gcc 编译一个 C 项目结果dnf install gcc直接报了一堆Failed to download metadata for repo appstream看着像网络问题折腾了半天才发现根子在 CentOS 8 的生命周期上。这个系统在 2021 年底已经正式 EOL官方源全部下线默认的 mirrorlist 基本连不上了所以哪怕你在内网、外网都配置好了 DNS、网关照样装不上任何包。如果你现在也卡在 CentOS 8 上装 gcc大概率会遇到同样的问题。这篇文章我把整个流程重新捋了一遍从源修复、在线安装、离线 rpm 方案到“gcc 升级后怎么还是旧版本”这种诡异问题一次性讲清楚。1. EOL之后源失效CentOS 8装gcc前必须先修repo1.1 dnf报错的典型表现CentOS 8 装 gcc 失败最常见的就是下面这类错误Error: Failed to download metadata for repo appstream: Cannot prepare internal mirrorlist: No URLs in mirrorlist或者更直接一点Errors during downloading metadata for repository baseos: - Status: 404这两种提示看着像 DNS 问题、防火墙问题、Yum 源配置写错实际上就是官方仓库已经不提供mirrorlist.centos.org的解析和重定向了。你配置里的mirrorlist参数指向一个已经没人管的地址自然什么也拉不下来。还有一部分机器表现比较“软性”dnf install gcc能跑但速度奇慢或者偶尔成功偶尔失败。这种情况通常是系统里还残留着某个能用的缓存等到缓存过期以后就会彻底暴露。1.2 改用vault源或国内镜像源的具体操作解决办法很直接把 repo 文件里的mirrorlist替换成baseurl指向 CentOS Vault 或国内镜像的 vault 目录。Vault 是 CentOS 官方对 EOL 版本做归档的地址CentOS 8 的所有 final 小版本都在里面。我这里以 8.5.2111 为例这是 CentOS 8 的最后一个版本也是绝大多数服务器实际跑的小版本。修改前建议先备份原始文件cd /etc/yum.repos.d/ mkdir backup mv CentOS-Linux-*.repo backup/然后新建一个干净的 repo 文件。如果你倾向用官方 Vaultcat /etc/yum.repos.d/CentOS-Vault.repo EOF [baseos] nameCentOS Linux $releasever - BaseOS baseurlhttp://vault.centos.org/8.5.2111/BaseOS/$basearch/os/ gpgcheck1 enabled1 gpgkeyfile:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial [appstream] nameCentOS Linux $releasever - AppStream baseurlhttp://vault.centos.org/8.5.2111/AppStream/$basearch/os/ gpgcheck1 enabled1 gpgkeyfile:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial [extras] nameCentOS Linux $releasever - Extras baseurlhttp://vault.centos.org/8.5.2111/Extras/$basearch/os/ gpgcheck1 enabled1 gpgkeyfile:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial [powertools] nameCentOS Linux $releasever - PowerTools baseurlhttp://vault.centos.org/8.5.2111/PowerTools/$basearch/os/ gpgcheck1 enabled0 gpgkeyfile:///etc/pki/rpm-gpg/RPM-GPG-KEY-centosofficial EOF如果服务器在国内官方 Vault 访问速度有时候不稳定我更推荐用阿里云的 vault 镜像路径规则一样只是域名不同https://mirrors.aliyun.com/centos-vault/8.5.2111/BaseOS/$basearch/os/写完以后清理缓存再验证dnf clean all dnf makecache dnf repolistdnf repolist能看到baseos、appstream、extras几个仓库的包数量就说明源已经活了。还有个小细节如果你的机器装的是 CentOS Stream 8不能用这套 vault 路径Stream 有单独的仓库地址别混用。判断方式很简单cat /etc/redhat-release输出里带Stream就和本文的仓库配置不通用。1.3 PowerTools仓库你迟早会用到的一个开关上面 repo 文件里我特意写了一个powertools仓库而且enabled0默认关闭。这不是随手添的而是 CentOS 8 一个非常典型的坑。gcc 本体在 AppStream 仓库里装 gcc 本身不依赖 PowerTools。但你做的是“装 gcc 用来编译”不是“装完就完事”。实际编译第三方库时经常需要gmp-devel、mpfr-devel、libmpc-devel这些和编译器配套的开发包其中一部分就在 PowerTools 仓库。等你dnf install gmp-devel报“没有匹配的包”时再回来开 PowerTools 就得重新折腾一遍。建议安装 gcc 之前直接启用它dnf config-manager --set-enabled powertools如果提示config-manager命令不存在先装dnf-plugins-corednf install -y dnf-plugins-core启用后再dnf repolist确认powertools处于 enabled 状态。这个步骤很不起眼但能帮你省掉后面编译各种依赖库时的一大堆“找不到包”的烦恼。2. 源修好后直接dnf在线装gcc、gcc-c和make是一条命令的事2.1 安装前建议执行的三个检查源修复后在线安装反而很简单但我不建议直接敲dnf install gcc就完事。先做三个检查能少踩很多坑。第一确认当前系统架构和版本uname -m cat /etc/redhat-releasex86_64 的机器就下 x86_64 的 rpmARM 的机器比如鲲鹏得用 aarch64 的包。这个在离线安装时尤其重要在线安装时 dnf 会自动匹配。第二确认仓库源已经全部可用尤其是 AppStream。gcc 的二进制包在 AppStream不在 BaseOS。很多人源修了一半只修了 BaseOS结果dnf install gcc依然报找不到包。第三看看系统里是不是已经装过旧版本rpm -qa | grep -E ^gcc- which gcc gcc --version 2/dev/null这一步是为了后面和“升级后还是旧版”的问题做区分。如果系统已经自带 gcc 8你后面装 Toolset 或者升级时不至于搞混到底谁是默认版本。2.2 用dnf安装gcc、gcc-c和make的正确姿势在 CentOS 8 上真正要敲的命令是这样的dnf install -y gcc gcc-c make很多人只装gcc结果编译 C 代码时g: command not found又跑回来问为什么。Linux 体系里gcc和g是两个独立包gcc只负责 Cgcc-c才提供 g 和支持 C 标准库的头文件链接。make不是 gcc 的依赖是独立的构建工具。如果你大部分场景是“configure make make install”没有 make 根本跑不起来所以顺手一起装掉。如果你要编译内核模块还要额外装dnf install -y kernel-devel kernel-headers这个看使用场景普通应用编译可以跳过。我每次都会一起装因为一台编译服务器上迟早会遇到 DKMS 或者自定义驱动编译的需求到那时再找 kernel-devel 往往还容易遇到内核版本不匹配的问题。2.3 hello world只是起点真正的验证要看这三行输出装完后不要只看“Complete!”就以为结束了。我习惯做三件事验证。第一版本确认gcc --version g --version make --versionCentOS 8 默认仓库里的 gcc 版本是 8.5.0。看到gcc (GCC) 8.5.0 20210514 (Red Hat 8.5.0-4)这种输出就是对的。第二实际编译一个 C 程序cat /tmp/test.c EOF #include stdio.h int main(void) { printf(gcc ok\n); return 0; } EOF gcc /tmp/test.c -o /tmp/test /tmp/test能输出gcc ok说明编译、链接、动态库加载都没问题。第三编译一个 C 程序验证libstdc头文件和运行时库正常cat /tmp/test.cpp EOF #include iostream int main() { std::cout g ok std::endl; return 0; } EOF g /tmp/test.cpp -o /tmp/test_cpp /tmp/test_cpp第三步非常重要。有些环境 gcc 装好了但 g 跑不了多半是libstdc-devel没装或者头文件链接坏了只有实际编译一个 C 文件才能暴露出来。这也是很多人装完 gcc 就以为万事大吉结果一到项目编译就报一堆iostream: No such file or directory的原因。3. 离线服务器装gcc用dnf download拉rpm再建本地repo3.1 在联网机器上一次性拉全依赖的两种命令很多生产服务器是内网隔离的外网根本不通。这种环境就别想着用 yum 在线安装了得走离线 rpm 包的路子。前提条件找一台和离线机同系统版本、同架构、能联网的 CentOS 8 机器在上面把 gcc 相关的所有 rpm 下载下来再拷贝过去。先装 dnf 插件dnf install -y dnf-plugins-core createrepo然后创建目录下载包和全部依赖mkdir -p /tmp/gcc-rpms cd /tmp/gcc-rpms dnf download --resolve --alldeps gcc gcc-c make--resolve会让 dnf 解析所有依赖--alldeps连已经安装过的依赖包也会一起下载。下载完检查一下ls -lh /tmp/gcc-rpms/正常情况下会有几十个 rpm包括gcc、gcc-c、binutils、cpp、glibc-devel、libstdc-devel、make等。数量少可能是没加--alldeps依赖不全的离线包后面装起来会非常痛苦。这里有个容易被忽略的问题在同一台联网机器上如果之前已经装过 gccdnf download --resolve下载的依赖中缺失的包数量会少一些因为已安装的 rpm 不会下载。所以尽量找一台干净的机器下载或者把目标机器上已有的 rpm 情况考虑进去。3.2 离线侧用本地目录搭一个临时yum源把/tmp/gcc-rpms目录整个拷到离线机我一般放在/opt/gcc-rpms。很多人拿到 rpm 后的第一反应是rpm -ivh *.rpm这是离线安装最大的坑。rpm 不会自动解析依赖顺序遇到 a 依赖 b、b 依赖 c、c 依赖 a 这种循环依赖时直接卡死。几十个包手工理依赖顺序纯属自虐。正确做法是用 createrepo 在联网机器上先建好仓库元数据然后离线机通过文件路径把它当成一个本地 yum 源来用。在联网机器上下载完 rpm 后直接生成 repodatacd /tmp/gcc-rpms createrepo .生成的repodata目录要和所有 rpm 放在一起整个拷走。然后离线机上写一个本地 repo 文件cat /etc/yum.repos.d/local-gcc.repo EOF [local-gcc] nameLocal GCC RPMS baseurlfile:///opt/gcc-rpms enabled1 gpgcheck0 EOF安装时禁用所有远程仓库强制使用本地仓库dnf --disablerepo* --enablerepolocal-gcc install -y gcc gcc-c make这样做的好处是dnf 会自动处理依赖关系顺序、循环依赖全部由包管理器解决体验和在线安装几乎一样。3.3 为什么我不建议用rpm -ivh手工按依赖顺序装有人会说我就在离线机上rpm -ivh *rpm试过一次也能装上。那多半是你运气好碰到的是一个顺序恰好正确的包集合或者依赖本来就完整。但生产环境最怕这种“碰运气”的做法。rpm -ivh遇到依赖缺失报错信息只是一句libmpc.so.3()(64bit) is needed by...你得上网查这个库是哪个包提供的然后再手工指定安装一层套一层极其浪费时间。而且某些 rpm 包之间存在“文件冲突”在一个错误的顺序下安装可能覆盖了另一个包的关键文件后面怎么修都修不回来。所以我始终坚持一个原则能走 dnf/yum 库就走库不要让 rpm 和依赖关系硬碰硬。离线环境下本地目录建 repo 是性价比最高的方案没有之一。另外还要注意一点拷文件的时候别只拷 rpm忘了repodata目录。很多人在这一步翻车本地 repo 配置好了dnf install却报Could not parse metalink或者找不到 repodata排查半天发现是repodata没拷过去。4. gcc升级后版本没变PATH、软链接和环境变量的排查顺序4.1 从type -a和which查起PATH里藏着的另一个gcc“gcc 升级后为啥还是旧版本”这个问题在搜索结果里出现的频率非常高。我自己的经验是十次里面有八次不是没升级成功而是系统里本来就有好几个 gccshell 找到的那个不是你以为的那个。排查第一步不是看版本而是看当前解析到的 gcc 到底在哪type -a gcc which -a gcctype -a会把 PATH 里所有能找到的gcc列出来并且标注是别名、函数还是磁盘文件。如果列出来多个路径比如gcc is /usr/local/bin/gcc gcc is /usr/bin/gcc那问题就明白了shell 按 PATH 顺序找到了/usr/local/bin/gcc这个 gcc 是旧的或者不是系统包管理器管理的。而你升级的是/usr/bin/gcc压根没被优先使用。看 PATH 顺序echo $PATH如果/usr/local/bin排在/usr/bin前面新装的系统版本就会被老的手工编译版本“拦截”。4.2 alternatives、软链接和被环境变量锁定的编译器第二种常见情况是软链接没切换。CentOS 8 里/usr/bin/gcc通常是一个符号链接ls -l /usr/bin/gcc输出可能是lrwxrwxrwx 1 root root 22 Aug 10 10:00 /usr/bin/gcc - /etc/alternatives/gcc再往下看ls -l /etc/alternatives/gcc update-alternatives --display gcc如果/etc/alternatives/gcc还指向旧版本的路径比如/usr/bin/gcc-8而你新装的是/usr/local/bin/gcc-11那么不管 rpm 列表里有多少个 gcc 版本系统默认用的还是老的。手动切换update-alternatives --install /usr/bin/gcc gcc /usr/local/bin/gcc-11 110 update-alternatives --config gcc第三种情况最容易忽略项目构建时根本不用 PATH 里的 gcc而是读环境变量CC或CXX。echo $CC echo $CXX如果你在~/.bashrc、/etc/profile或 CI 脚本里写过export CC/usr/bin/gcc-8那不管你怎么升级gcc --version 是新版项目编译用的还是老编译器。CMake 项目还要多查一层缓存文件CMakeCache.txt里可能记录了第一次配置时的编译器路径即使环境变了也不会自动更新。遇到这种情况删除CMakeCache.txt重新 configure 才是正解。4.3 一个容易被忽视的元凶IDE自带gcc并不注册到系统还有一个搜索热词是“MounRiver Studio的gcc安装到了哪里”。这类 IDE 通常会自带一套独立工具链比如 RISC-V GCC、ARM GCC放在 IDE 安装目录的某个子目录里并不会注册到系统 PATH也不会和/usr/bin/gcc产生关系。所以你在系统里怎么找都找不到 IDE 的 gcc是正常的。它自己编译的时候用的是相对路径或者 IDE 内部配置的完整路径跟你系统里装没装 gcc 没有半毛钱关系。反过来如果你在 IDE 里配置编译器时填了系统路径结果 IDE 报找不到编译器那多半是系统里根本没有装 gcc或者装了但 PATH 不可见。先确认ls -l /usr/bin/gcc存在的话在 IDE 里手动填/usr/bin/gcc即可。5. 别急着编译源码用gcc-toolset在CentOS 8上换新版本编译器5.1 dnf module里的gcc-toolset到底怎么用CentOS 8 默认仓库里的 gcc 是 8.5.0对新标准支持有限。如果你要编译 C17/20 特性、新版 OpenMP或者某个库要求 gcc 10别第一时间去网上找源码包自己编译CentOS 8 官方仓库本身就有方案——gcc-toolset。先看看可用版本dnf module list gcc-toolsetRHEL/CentOS 8 的 AppStream 里带了多个 GCC Toolset常见的有 gcc-toolset-9、gcc-toolset-10、gcc-toolset-11 等。不同小版本范围不一样8.5 上一般能看到 9 到 11。装 gcc-toolset-11dnf install -y gcc-toolset-11-gcc gcc-toolset-11-gcc-c装完后gcc 并不会立刻变成 11官方工具链安装在独立目录里。使用时要手动启用source /opt/rh/gcc-toolset-11/enable或者scl enable gcc-toolset-11 bash启用后再看版本gcc --version g --version就会变成 GCC 11 系列。这里要注意scl enable gcc-toolset-11 bash是开启一个子 shell退出 shell 后环境变量就没了。如果你希望某个用户登录即生效在~/.bashrc里追加source /opt/rh/gcc-toolset-11/enable但我不建议全局默认启用除非你确认这台机器上所有项目都兼容新版本编译器。最稳的方式是构建脚本里显式source构建完退出。5.2 使用新版本gcc后动态库兼容性怎么处理很多人用 gcc-toolset 编译没问题但把编译出来的二进制部署到其他机器后运行时报./app: /lib64/libstdc.so.6: version GLIBCXX_3.4.29 not found (required by ./app)这是因为 gcc-toolset-11 默认动态链接的是/opt/rh/gcc-toolset-11/root/usr/lib/gcc/...下的新版libstdc.so.6而目标机器的/usr/lib64/libstdc.so.6还是老版本里面没有GLIBCXX_3.4.29这个符号。三种解决方案按优先级排列。方案一部署时带上目标机器缺失的库。先用ldd查ldd ./app | grep libstdc看到路径后把对应的libstdc.so.6一起拷贝到目标机器的可执行文件同目录或者放到LD_LIBRARY_PATH里。方案二链接时静态链接 C 标准库g -static-libstdc -static-libgcc ...缺点是二进制体积变大但部署省心很多对于要发到多台内网机器的场景非常实用。方案三运行时指定export LD_LIBRARY_PATH/opt/rh/gcc-toolset-11/root/usr/lib64:${LD_LIBRARY_PATH}这个方案只适用于目标机器本身也装了 gcc-toolset 的情况局限性很大。5.3 我的建议什么时候该上Toolset什么时候继续用8.5gcc-toolset 确实是好东西但不要盲目切换。我的判断标准很简单如果只是编译普通的 C 代码、简单的 C 程序系统自带的 gcc 8.5 完全够用稳定性和兼容性都经过大量验证。尤其是 CentOS 8 默认依赖、第三方预编译库、驱动模块都是基于系统 gcc 编译的你换新编译器编译应用没问题但一旦和这些组件混着链接容易出幺蛾子。如果项目明确要用 C17/20 新特性或者依赖库要求 gcc 10那就用 gcc-toolset而且尽量在干净的构建环境里编译产物用静态链接标准库的方式分发。另外提醒一点一个项目最好保持编译器一致不要 gcc 8 编译的库和 gcc 11 编译的应用混合链接libstdc的 ABI 在新版本里虽然有兼容层但跨大版本混用依然是很多诡异运行错误的根源。根据我个人经验还有一个小技巧装完 gcc 或 gcc-toolset 后建议在/etc/profile.d/下留一个gcc-toolset-env.sh把需要source的路径和版本注释得清清楚楚这样后续接手这台机器的人不会一头雾水。运维这种事省下的每一分钟都是在给自己留余地。