11 月的主线:把存储域的 swap 收尾,正式转入「软件包与仓库」。这个月开始每个能力域配一本实操手册,随进度长。新内容在最上面,每条末尾的「能力索引」指向 RHCSA 学习地图。

2026-11-05 星期四

今天做了什么

  • 把 S3 日志这块收了尾:systemd-lab.html 从 20 步补到 23 步,补的三步是「持久化」「保留策略」「上限配置」。
  • 持久化实测两条路:mkdir + systemd-tmpfiles,和 Storage=persistent。两条都能让日志活过重启,差别只在目录权限 —— 而权限决定了组员能不能读。
  • 保留策略跑全了:--rotate 切出归档,再分别用 --vacuum-size / --vacuum-time / --vacuum-files 裁。
  • 上限用 drop-in 写 SystemMaxUse=20M,max 从 1.0G 变成 20.0M,rotate 后单文件从 8M 降到 2.5M。
  • 顺手摸了 S4 的环境:cronie 已装、crond 是 enabled + active、/etc/crontab 在;at 没装,atd.service 都不存在,S4 第一步得先补它。

卡住的地方

  • 「restart 之后 /var/log/journal 里有没有文件」,我原来的说法是「没有,要 --flush 才出现」。他说「并不空」—— 他是对的。重测:restart 一返回,<machine-id>/ 和 8MB 的 system.journal 当场就在。--flush 干的是把 /run 里已有记录搬过去,而 restart 启动时自己已经刷过一遍,所以再敲 --flush 一行输出都没有。两次实测结论不一致时,要换一个更硬的判据,而不是挑自己更喜欢的那个。
  • --disk-usage 从 1.6M 涨到 8.0M,我第一反应是「条目变多了」。其实那个 8MB 文件里当时只有 17 行 —— 涨的是预分配空间,不是内容。
  • --vacuum-size=1M 打印 freed 3.6M,我看 --disk-usage 还有 8.0M 就以为没降到位。其实总量是从 11.6M 降到 8.0M,正好少 3.6M;活动文件永远不会被 vacuum 删掉,那 8.0M 是下限。
  • 撤销 drop-in 时被 root 的 rm -i 卡住:敲 rm 之后它问 remove regular file …?,我紧接着敲的 systemctl restart systemd-journald 被当成回答吃掉了 —— 文件没删、服务也没重启,max 一直显示 20.0M,看着像「配置改不回去」。用 \rm 或 rm -f 才对。
  • echo '[Journal]\nSystemMaxUse=20M' 会把 \n 原样写进文件,得用 printf 或 echo -e。

关键命令或配置

1
2
3
4
5
# 持久化:建目录 → 修权限 → 重启(不需要 --flush)
mkdir -p /var/log/journal
systemd-tmpfiles --create --prefix /var/log/journal
systemctl restart systemd-journald
journalctl -u systemd-journald -b --no-pager | grep "Journal (" # Runtime → System
1
2
3
4
# 裁日志:先切归档,再裁
journalctl --rotate
journalctl --vacuum-size=1M
journalctl --disk-usage
1
2
3
# /etc/systemd/journald.conf.d/99-lab.conf
[Journal]
SystemMaxUse=20M
1
2
systemctl restart systemd-journald
systemd-analyze cat-config systemd/journald.conf | tail -6

今天真正记住的判断

  • 上限 ≠ 目标值:设了 1.0G,--disk-usage 停在 11.6M 是正常的 —— 没到阈值它不清理。
  • 判配置是否生效看 MainPID 变没变,比 grep 可靠。grep 不加 tail -1 会读到上一条旧记录。
  • ls -l 是表观大小,du 是实际块:journal 文件按固定槽位预分配,归档后两个数能差一倍(8MB vs 3.7M)。
  • cat /etc/xxx.conf 只看到一份配置,systemd-analyze cat-config 才能看到主文件 + 所有 drop-in 合并后的结果。

下一步

S4 计划任务:先把 at 装上(dnf install -y at + systemctl enable --now atd),再走 cron(crontab -e/-l/-r、/etc/cron.d、run-parts、/var/log/cron)→ systemd timer(.timer + .service 配对、OnCalendar、list-timers)。考纲三样都要。

能力索引 —— 今天涉及的知识点在地图上的位置:

2026-11-04 星期三

今天做了什么

  • S1 服务生命周期一次跑完:disable → stop / start → mask → unmask。四象限全造出来了,还自己多试了一步 —— mask 状态下 enable 也失败,不只是 start。
  • 用 MainPID 把 restart 和 reload 分开取证:restart 7565 → 7922,reload 8025 → 8025。顺手查了 CanReload,crond、sshd 是 yes,chronyd、dnf-makecache 是 no;硬 reload 一个 no 的 unit,退出码是 3,不是 1。
  • enable --now / disable --now 实测;static 这一类(本机 96 个)单独测了一轮。
  • S2 target:default.target 是一条软链、不是服务;set-default 同值是幂等的;graphical 是 multi-user 的超集;isolate 和 set-default 各管一头。
  • S3 前半 journald:日志到底存在哪、-p 的语义、debug 级只进 journal 不进 messages、结构化字段、--flush 在没持久化时静默成功、systemd-tmpfiles 造出来的目录权限。
  • 手册侧:systemd-lab.html 今天从 6 步长到 14 步(服务生命周期 10 步 + target 4 步),日志这块再补 6 步。

卡住的地方

  • 第 2 问「stop 之后为什么 is-enabled 还是 enabled」,我答成了「is-enabled 是干什么的」。问的是为什么,答的是是什么 —— 正解是 stop 和 enable 改的是两个不同的东西,两条路径根本不通信。
  • --no-paper、-1:真参数是 --no-pager、-l(小写 L)。打错时 systemd 只甩一句 unrecognized option。
  • list-unit-files --type=service --state=running → 0 unit files listed.,stderr 全空、退出码 1;接上 | wc -l 之后变成 0 加退出码 0。看着像「没有服务在跑」,其实是这个参数它不认 —— running 是 sub-state,list-unit-files 只收 load state 和 active state。
  • ls -l $pwd/crond.service → /crond.service。shell 变量是大写 $PWD,$pwd 未定义就静默展开成空串(bash 默认行为,set -u 才会报)。
  • 在 wants 目录里 wc -l crond.service 得到 15 —— wc 跟随软链读了真 unit 文件,而 ls -l 看的是链接本身。同一条路径,不同命令看不同层。
  • mask 之后在 wants 目录 ls,看到还指向 /usr/lib,一度以为 mask 没生效。其实那是自启链接,mask 盖的是 /etc/systemd/system/crond.service 这个同名文件,两个不同的东西。
  • --state=enable(少一个 d)同样返回 0 个、stderr 空。
  • 我给「只进 journal、不进 messages」写的演示命令是 logger -t labtest "hello",默认 priority 是 info,两边都有。cr 一句「不都有吗」就把我堵住了。真正的例子是 debug 级。给演示命令之前,必须先验证它真能证明标题里的结论。
  • journalctl --flush 在 /var/log/journal 不存在时:零输出、退出码 0,什么也没做。

关键命令或配置

1
2
3
4
5
6
# mask 之后:Loaded 是 masked,Active 还是 active
systemctl mask crond
systemctl status crond --no-pager # Loaded: masked / Active: active (running)
systemctl start crond # Failed ... Unit crond.service is masked.
systemctl enable crond # 也失败:读不到 [Install] 段 → 顺序只能 unmask → enable
systemctl unmask crond; systemctl is-enabled crond # 直接回到 enabled
1
2
3
4
5
# restart 换进程,reload 不换
systemctl show -p MainPID --value crond # 7565
systemctl restart crond # → 7922
systemctl show -p CanReload chronyd # no
systemctl reload chronyd # Job type reload is not applicable → 退出码 3
1
2
3
4
# static:没有 [Install] 段,enable 返回 0 却什么都没建
systemctl is-enabled dnf-makecache.service; echo $? # static / 0
systemctl cat dnf-makecache.service | grep Install # 无输出,exit 1
systemctl enable dnf-makecache.service; echo $? # 729 字节告警打到 stderr,但退出 0
1
2
3
4
# target 是一条软链,不是服务
ls -l /etc/systemd/system/default.target
systemctl set-default multi-user.target # 同值:无输出、exit 0、mtime 不变
systemctl isolate multi-user.target # 只改现在,get-default 不变
1
2
3
4
5
6
7
8
9
# 日志:先确认持久化开没开
ls /run/systemd/journal/ # 这里只有 socket,不是数据
ls /run/log/journal/*/ # 数据在这,/run 是 tmpfs → 重启即丢
ls -d /var/log/journal # 不存在 = 没开持久化
logger -p debug -t labdebug "debug line only"
journalctl -t labdebug # 有
grep labdebug /var/log/messages # 无 —— *.info 那行把 debug 丢掉了
journalctl -p 3 --no-pager | wc -l # 13
journalctl -p 2 --no-pager | wc -l # 7 ← 数字越小越严重,-p 3 意思是 ≤ err

几条今天真正记住的判断

  • stop / enable 是两条不通信的路径:一条动进程,一条动 wants 里的链接。isolate 和 set-default 是同一个结构 —— 一个管现在,一个管下次。
  • 这一域的退出码几乎不能当判据:enable 一个 static 服务返回 0 却什么都没建;is-enabled 对 enabled 和 static 也都返回 0,只看文本才分得出来。
  • --state 是过滤器,不是校验器。零结果先怀疑参数拼错,别怀疑机器。
  • mask 是「禁止存在」,不是「禁止自启」:Loaded: masked 的同时 Active: active (running) 是对的,它只回答「以后还能不能启动」。
  • -p err 是「≤ err」,不是「= err」。数字越小越严重,-p 2 一定比 -p 3 少。
  • journald 是上游,rsyslog 是下游消费者:进程 → journald → imjournal → rsyslog → messages / secure / cron。所以「两边都有」是常态,要找只进 journal 的东西得挑它过滤掉的级别(debug)。
  • /run 是 tmpfs,所以默认日志重启就丢。持久化的开关不是改某个配置项,而是建不建 /var/log/journal 这个目录。

下一步

  • 日志持久化实操:mkdir /var/log/journal → systemd-tmpfiles --create --prefix /var/log/journal → restart systemd-journald → --flush → 验证目录里真有文件。今天只验到 tmpfiles 把权限改成 root:systemd-journal 2755,journald 还没重启过。
  • 保留策略:SystemMaxUse= / MaxRetentionSec= / --vacuum-size / --vacuum-time / --rotate。
  • 然后 S4:cron / at / systemd timer。

今天的配套手册

  • systemd 实操手册 —— 今天长到 20 步:第 1 章服务生命周期(T1–T10)、第 2 章 target(T11–T14)、第 3 章 journald(T15–T20,最后一步持久化明确标了「尚未在本机验证」)。
  • 实操手册总览 —— 所有手册的入口和进度。

能力索引 —— 今天涉及的知识点在地图上的位置:

2026-11-03 星期二

今天做了什么

  • 软件包域 P12–P16 一次走完:history undo、versionlock、provides、软件包组、module 流。其中 provides 三条、module 那块(enable → install → list –installed → reset)都对了。
  • 但 versionlock 连着三次实验都不成立,最后干脆自己上机(root 免密)把疑点一个个测掉:锁只作用在求解层、--disableexcludes=all 绕不过去、显式写全 NVRA 也被过滤、对没安装的包执行 add 会把仓库里所有版本一起锁上。
  • 顺手把 rpm 直接操作 P17–P21 在实机上全跑了一遍(rpm -ql/-qc/-qR、rpm -V 三态、rpm2cpio 还原、--test 试穿、卸载边界)。
  • 查了 EX200 考纲原文:rpm -V / rpm2cpio / --nodeps 都不在考纲里,考纲说的「从本地文件系统安装」标准做法是 dnf install /路径/x.rpm。还有一条更要紧的:EX200 现在已经基于 RHEL 10,实验机是 Rocky 9,练手可以,但 dnf5 和 module 的差异要以 RHEL 10 为准。
  • 开新域 systemd:先取基线(get-default = multi-user.target、四个服务全是 enabled+active、multi-user.target.wants/ 里全是符号链接),环境太干净没有现成样本,就用 crond 人为造出「disabled 但 active」的对照,把 mask 的机制测通了。
  • 博客侧:做了「实操手册总览」页,又把手册做成正式文章归到「教程」分类下;顺手修了一处浅色主题下正文发白的问题 —— --light-grey 在 Butterfly 浅色主题里是 #eee,名字像文字色,实际是极浅的描边色。

卡住的地方

  • 第一轮拿 tree 测锁:dnf update tree 输出 Nothing to do.,我以为锁生效了。其实 tree 本来就是仓库里唯一版本 —— 拿一个没有升级路径的包测锁,等于没测。
  • 第二轮我设计「锁前 / 锁后各跑一次 dnf list --upgrades」做对照,实测两次都显示 127。这个对照设计本身是错的,因为 list 走查询层,压根不看锁。
  • dnf history info latest 不报「参数非法」,报 No transaction which manipulates package 'latest' was found —— 那个位置也能被当成包名。正解是 last。
  • rpm2cpio x.rpm | cpio -idmv etc/ssh/sshd_config 少写一个 ./:一个文件都没提取,却照样打印 2992 blocks、退出码 0。
  • rpm -ivh --test /tmp/nonexist.rpm | head 之后 echo $? 是 0,去掉管道才是 1。管道把退出码换成了右边那个命令的。
  • 恢复 sshd_config 的 mtime 手写 20:05:00 不行,真值 20:05:41,最后靠 touch -r 借包内原文件的时间戳才让 rpm -V 彻底无输出。

关键命令或配置

1
2
3
4
5
6
7
# versionlock 的正确取证:两层各取一次,缺一不可
dnf check-update # 先找一个真有更新的包(有则退出码 100)
dnf list --upgrades grub2-tools # ① 查询层:候选确实存在(不受锁影响)
dnf versionlock add grub2-tools
dnf list --upgrades grub2-tools # 仍然显示新版本 —— 这很正常
dnf update grub2-tools # ② 求解层:Nothing to do. ← 锁在这里生效
dnf versionlock delete grub2-tools # 记得解锁
1
2
3
4
5
6
7
8
# rpm -V 的三态:只有第 3 位(内容摘要)亮才是真被改了
rpm -V openssh-server # 基线:无输出,退出 0
echo '# lab' >> /etc/ssh/sshd_config # → S.5....T. c
chmod 644 /etc/ssh/sshd_config # → .M.....T. c
chmod 600 /etc/ssh/sshd_config # → .......T. c ← T 永远消不掉,mtime 也被校验
dnf download --destdir /tmp/fix openssh-server-9.9p1-9.el9_8.rocky.0.1 # 必须写全 NVRA
rpm2cpio /tmp/fix/openssh-server-*.rpm | cpio -idmv ./etc/ssh/sshd_config # ./ 不能省
touch -r /tmp/fix/etc/ssh/sshd_config /etc/ssh/sshd_config # 借原始时间戳
1
2
3
4
5
6
7
8
# systemd:两个维度分开验,别用 status 一句话概括
systemctl disable crond # Removed ".../multi-user.target.wants/crond.service".
systemctl is-enabled crond # disabled
systemctl is-active crond # active ← 服务还在跑,只是下次开机不起来了
systemctl mask crond # 建 /etc/.../crond.service → /dev/null
systemctl start crond # Failed ... Unit crond.service is masked.
systemctl unmask crond # is-enabled 直接回到 enabled(wants 链接一直没动过)
systemctl show -p FragmentPath crond # 问它实际从哪个路径读的 unit

几条今天真正记住的判断

  • 「没报错」和「跑完了」都不是验证。 任何查询类输出(Nothing to do、空列表、0 条结果)都得能回答一句:如果它有东西可做,会显示成什么样? 答不上来,这条命令就不能当验收。
  • 同一份配置,会被不同层的命令给出不同答案。 判断「是否生效」前先问:这条命令属于哪一层?存储域里是 parted Flags(分区表层)vs blkid(设备签名层)、swap 的类型 GUID vs mkswap 签名;软件包域里是 list / repoquery(查询层)vs update / install(求解层),versionlock 只塞进后者。
  • 命令的语义有一部分写在配置文件里。 clean_requirements_on_remove=True 让 dnf remove htop 把 hwloc-libs 一起清掉,这个场景下 remove 与 undo 结果完全相同。undo 真正不可替代的是三种:升级后能降级、remove 后能装回、一个事务动了 N 个包能整体回退。
  • 「现在跑着吗」和「开机自启吗」是两个独立问题。 is-active 和 is-enabled 各答一个,四种组合都真实存在;enable 的实质就是 wants 目录里那条符号链接。
  • mask 不是「禁止自启」,是「禁止存在」 —— 往 /etc 放一个指向 /dev/null 的空 unit,把 /usr/lib 里的真 unit 盖掉。unit 查找顺序 /etc > /run > /usr/lib。
  • rpm -qc 只列文件级的配置,目录永远不列(所以比 -ql | grep ^/etc 少一行);一次查多个包时,只要有一个没装,退出码就是 3。

下一步

  • systemd 按依赖顺序走:S1 服务生命周期(样本已经备好)→ S2 target 与默认目标 → S3 journalctl 与日志持久化 → S4 cron / at / timer → S5 chronyd + timedatectl → S6 中断启动取 root。
  • 范围只认考纲:自己写 .service 文件、socket activation、cgroup 原理都标选做,不占主线。

今天的配套手册

  • 软件包实操手册 —— P13 重写了:删掉那个「锁前锁后对比 list」的错误设计,换成「候选存在 + update 不动」的双层判据,并补上 --disableexcludes 绕不过、未安装包 add 是陷阱。
  • 实操手册总览 —— 所有手册的入口和进度都在这一页。
  • 存储手册今天没动。

能力索引 —— 今天涉及的知识点在地图上的位置:

2026-11-02 星期一

今天做了什么

  • 把存储域最后一块 swap 做完了(分区版全流程):parted mkpart → partprobe → mkswap → blkid → 写 fstab → swapon -a → 验收。swapon --show 出两行,free -h 的 Swap 从 820M 变成 1.3Gi。还没重启验证,先挂着。
  • 存储手册加了实验 D(swap 分区 + swap 文件,D1–D8);其中 swap 文件那一步只是写在纸上,本机还没做过。
  • 开了新的能力域:软件包与仓库。先弄清「仓库到底是什么」,再搭一个本地 file:// 仓库,P1–P11 走完。
  • 定下一条规矩:以后每做完一个能力域就配一本 <域>-lab.html,实验用独立小节隔开,随进度长 —— 存储的 A/B/C/D 就是模板。

卡住的地方

  • 以为「把 rpm 放进目录,dnf 就能装」。实际 dnf 只去 <baseurl>/repodata/repomd.xml 取索引,从不扫目录。故意先不跑 createrepo_c 试了一次,makecache 报 Curl error (37): Couldn't read a file:// file,正好对上。单个 rpm 走的是另一条路:dnf install ./x.rpm。
  • 以为 mount -a 会把 swap 也带上。结构上不可能:mount 需要一个挂载点,而 swap 的 fstab 第 2 字段就是 none;内核里是 mount(2) 和 swapon(2) 两条不同的系统调用。
  • .repo 里把 enabled=1 手滑写成 enable=1,dnf 照跑不误 —— 它不校验未知键,而 enabled 默认值就是 1,等于蒙对了。★ 反方向才致命:写 enable=0 想关仓库会静默无效、毫无提示。
  • grep 找 [crb] 那一段:[ 在正则里是字符类起点,必须转义。试了 grep '\$crb\$'、grep '\$crb\$' 全无匹配,最后 grep '\[crb\]' 才对;最省事的是 grep -F '[crb]'。

关键命令或配置

1
2
3
4
5
6
7
8
9
# swap(分区版),起点照 parted print free 的结果写,不要照抄
parted /dev/sdb mkpart primary linux-swap 1025MiB 1537MiB
partprobe /dev/sdb
mkswap /dev/sdb2
blkid /dev/sdb2 # 必须看到 TYPE="swap",这才是内核认的签名

echo 'UUID=<uuid> none swap defaults 0 0' >> /etc/fstab
swapon -a
swapon --show && free -h # df 永远看不到 swap
1
2
3
4
5
6
7
8
9
10
11
12
13
# 本地 file:// 仓库的最小闭环
dnf install -y createrepo_c
dnf download --destdir=/opt/localrepo tree htop
cat > /etc/yum.repos.d/local.repo <<'EOF'
[local]
name=local
baseurl=file:///opt/localrepo
enabled=1
gpgcheck=0
EOF
createrepo_c /opt/localrepo # 少了这步,makecache 必报 Curl error (37)
dnf makecache
dnf repolist --all | grep local

几条今天真正记住的判断

  • swap 有两套互不校验的身份:① 分区表里的类型标记(parted 写的,纯标签,print 的 Flags 列能看到);② 分区开头约 4 KiB 的签名(mkswap 写,blkid 出 TYPE="swap")。内核只认 ②,所以「Flags 里标了 swap」不能当验收。
  • swapon --show 里系统盘显示 /dev/dm-1 是内核临时编号,重启可能变 dm-2 —— fstab 绝不能写 dm-N,要写 UUID= 或 /dev/mapper/vg-lv。
  • 只写 fstab、不跑 swapon -a,重启照样生效:开机读 fstab 的不是 swapon 命令,而是 systemd-fstab-generator 把每行翻译成 .swap unit。预演的价值在于把失败挪到有人看着的时候(命令行当场报错 vs 开机静默丢/进 emergency)。
  • dnf install 那两行大小是两层:先 4.3 kB 取 repomd.xml(索引目录),确认变了才拉 44 MB 的全量元数据。所以 clean all 之后必然两行,平时只有第一行才正常。
  • --disablerepo="*" 下装 htop 报 nothing provides libhwloc.so.15,不是本地仓库坏:依赖求解只在「当前启用的仓库集合」里做,hwloc-libs 在 appstream 里被一起关掉了。这条命令只适合证明「某包来自本地仓库」。
  • 仓库跟着文件走,不跟名字走:把 rocky.repo 改名,baseos 和 appstream 一起消失,extras 还在(它住在 rocky-extras.repo)。实测 7 个 .repo 文件 → 48 个仓库定义 → 5 个启用,三个数字互不相等。
  • --enablerepo(只管这一条命令)、--set-enabled(改文件、持久)、--add-repo(写出新文件)三者别混。--set-enabled crb 之后 grep 能看到 enabled 从 0 被改成 1 —— 这就是「临时」和「持久」的实证。

下一步

  • 重启验 swap:free -h + swapon --show;若只剩 820M,先 journalctl -b | grep -i swap。
  • 软件包从 P12 接:dnf history / history undo → dnf provides → versionlock → 组与 module 流 → rpm 直接操作。

今天的配套手册

  • 存储实操手册 —— swap 已补成实验 D(D1–D8),存储域四个实验至此齐全。
  • 软件包实操手册 —— 今天 P1–P11 走的就是这一本:本地 file:// 仓库、依赖跨仓库、--set-enabled 与 --enablerepo 的区别都在里面。
  • 实操手册总览 —— 所有手册的入口和进度,以后从这里进。

能力索引 —— 今天涉及的知识点在地图上的位置: