/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
这篇是什么
一篇按任务组织的 Linux 操作手册:开端口、装软件、传文件、加用户、挂磁盘、写服务……每件事给出 CentOS 7.9、Rocky 9.8、Ubuntu 24.04.5 三个系统上的写法。不建议从头读,用目录跳,或者直接 Ctrl+F 搜你要做的事、你看到的报错。
每条命令都在三台机器上跑过,贴的是真实输出(太长的做了截取,已注明)。同一条命令三台结果不一样的地方,三台都贴。
同一套机器上还写过一个九篇的连载《Linux 实战》,专门挖坑(「为什么这么做不生效」)。这篇讲「怎么做」,讲到坑时会提一句,细节去连载里看。
0. 实测环境,以及怎么看这篇
| c7 | CentOS Linux 7.9.2009 | 3.10.0-1160.71.1.el7 | VMware 虚拟机,我自己用了几年的机器,xfs + LVM |
| rocky9 | Rocky Linux 9.8 (Blue Onyx) | 5.14.0-687.49.1.el9_8 | LXD 虚拟机,云镜像,补装了 firewalld、SELinux enforcing、core 组 |
| ubuntu2404 | Ubuntu 24.04.5 LTS | 6.8.0-139-generic | LXD 虚拟机,云镜像,ext4 |
几个会影响输出的事实,先说在前面:
- Rocky 和 Ubuntu 是云镜像,比 ISO 装出来的系统少很多包(比如我这台 Ubuntu 没有 man,Rocky 没有 tar)。所以本文不会说「某系统默认没有某命令」,只说「我这台没有」,并在第 3 章给出一张实测过的「命令找不到 → 装哪个包」对照表。
- CentOS 那台是真实用了几年的机器:root 的 .bashrc 里启用了 devtoolset-7,系统 locale 是 zh_CN.UTF-8,还有一个坏掉的软件源。这些都会在输出里露出来,遇到时会注明,正好也是真实环境里会碰到的情况。
- 所有命令都用 root 执行,除非特别说明。
- 输出里的 IP:c7 是 192.168.3.101;两台 LXD 虚拟机在 10.115.180.0/24,rocky9 是 .19,ubuntu2404 是 .200,宿主机网桥是 .1。
目录
- 1 我在哪个系统:发行版、版本、内核、虚拟化
- 2 开放端口:firewalld / ufw / iptables
- 3 装软件:yum / dnf / apt、本地包、锁版本、命令找不到装哪个包
- 4 基础命令:导航、查看、帮助、重定向、通配符
- 5 文件与目录:复制、移动、删除、权限、属主、链接、查找
- 6 文本处理:grep / sed / awk / sort / cut / diff / vim
- 7 压缩与解压:tar / gzip / xz / zip,Windows 中文 zip
- 8 上传下载:scp / rsync / sftp / curl / wget,从 Windows 传
- 9 网络:看配置、测连通、查端口、DNS、抓包、改 IP
- 10 用户与权限:useradd / passwd / sudo / 组 / 过期锁定
- 11 进程与服务:ps / kill / 后台运行 / systemctl / 自己写服务
- 12 磁盘:查空间、分区挂载、fstab、LVM 扩容、swap、NFS
- 13 日志:journalctl / 日志文件 / logrotate
- 14 定时任务:crontab / cron.d / systemd timer
- 15 SSH:密钥、config、端口转发、加固
- 16 性能:CPU / 内存 / IO / 内核参数
- 17 SELinux 与 AppArmor
- 18 时间时区、中文、脚本常见报错,以及报错速查
1. 我在哪个系统:发行版、版本、内核、虚拟化
1.1 最该先跑的一条:cat /etc/os-release
三台都有这个文件。关键是 ID 和 ID_LIKE 两行:ID_LIKE 含 rhel 的按 RHEL 系处理(yum/dnf、firewalld),含 debian 的按 Debian 系处理(apt、ufw)。
# CentOS 7.9 # Rocky 9.8 # Ubuntu 24.04.5
NAME="CentOS Linux" NAME="Rocky Linux" NAME="Ubuntu"
VERSION="7 (Core)" VERSION="9.8 (Blue Onyx)" VERSION="24.04.5 LTS (Noble Numbat)"
ID="centos" ID="rocky" ID=ubuntu
ID_LIKE="rhel fedora" ID_LIKE="rhel centos fedora" ID_LIKE=debian
VERSION_ID="7" VERSION_ID="9.8" VERSION_ID="24.04"
(三台的输出截取了同名字段,并排放在一起方便对比。)
脚本里只要一个值时,放进子 shell 里读,不污染当前变量:
# ( . /etc/os-release; echo "ID=$ID VERSION_ID=$VERSION_ID PRETTY=$PRETTY_NAME LIKE=$ID_LIKE" )
ID=rocky VERSION_ID=9.8 PRETTY=Rocky Linux 9.8 (Blue Onyx) LIKE=rhel centos fedora
# grep '^ID=' /etc/os-release | cut -d= -f2 | tr -d '"'
centos
1.2 其它几个版本文件
# cat /etc/redhat-release # CentOS / Rocky 有
CentOS Linux release 7.9.2009 (Core)
Rocky Linux release 9.8 (Blue Onyx)
# cat /etc/debian_version # Ubuntu 上是 Debian 的基线版本,不是 24.04
trixie/sid
# cat /etc/issue # RHEL 系里是转义符,不是明文
\\S
Kernel \\r on an \\m
lsb_release -a 在我这台 CentOS 和 Rocky 上都是 command not found,Ubuntu 上有。写脚本别依赖它,用 /etc/os-release。
1.3 内核、架构、主机信息
# uname -r
3.10.0-1160.71.1.el7.x86_64 # CentOS 7
5.14.0-687.49.1.el9_8.x86_64 # Rocky 9
6.8.0-139-generic # Ubuntu 24.04
# uname -m # 架构,和 arch 一样
x86_64
信息最全的一条是 hostnamectl,系统、内核、虚拟化一次看全:
# hostnamectl (CentOS 7)
Static hostname: localhost.localdomain
Virtualization: vmware
Operating System: CentOS Linux 7 (Core)
Kernel: Linux 3.10.0-1160.71.1.el7.x86_64
Architecture: x86-64
1.4 是物理机、虚拟机还是容器
# systemd-detect-virt; echo "rc=$?"
vmware # CentOS(VMware)
kvm # Rocky / Ubuntu(LXD 虚拟机底层是 QEMU/KVM)
rc=0
# systemd-detect-virt -c; echo "rc=$?" # 只判容器
none
rc=1 # 不是容器时输出 none,退出码 1
# dmidecode -s system-manufacturer # 需要 root
VMware, Inc.
QEMU
(我这台 Ubuntu 云镜像没装 dmidecode,command not found。)
1.5 一段脚本拿全景(三台都跑过)
echo "===== 系统 ====="
( . /etc/os-release 2>/dev/null && echo "$PRETTY_NAME (ID=$ID LIKE=$ID_LIKE)" ) || cat /etc/redhat-release 2>/dev/null || cat /etc/issue
echo "===== 内核/架构 ====="
uname -srm
echo "===== 虚拟化 ====="
systemd-detect-virt 2>/dev/null || echo "(无 systemd-detect-virt)"
echo "===== 包管理器 ====="
for p in dnf yum apt; do command -v $p >/dev/null 2>&1 && echo "有:$p"; done
echo "===== 防火墙 ====="
for p in firewall-cmd ufw nft iptables; do command -v $p >/dev/null 2>&1 && echo "有:$p"; done
echo "===== init ====="
ps -p 1 -o comm=
Ubuntu 上的输出:
===== 系统 =====
Ubuntu 24.04.5 LTS (ID=ubuntu LIKE=debian)
===== 内核/架构 =====
Linux 6.8.0-139-generic x86_64
===== 虚拟化 =====
kvm
===== 包管理器 =====
有:apt
===== 防火墙 =====
有:ufw
有:nft
有:iptables
===== init =====
systemd
CentOS 上包管理器只有 yum,Rocky 上 dnf 和 yum 都有(Rocky 9 的 yum 就是 dnf)。
2. 开放端口:firewalld / ufw / iptables
2.1 先看清三种「不通」的报错
我在每台上起一个 8080 的 Web 服务,从另一台机器用 curl 访问。报错长什么样,直接告诉你是谁挡的:
| curl: (7) Failed to connect … No route to host | firewalld 默认规则拒绝(CentOS、Rocky) |
| curl: (28) Connection timed out | ufw 默认 deny(Ubuntu),包被直接丢掉 |
| curl: (7) Failed to connect … Connection refused | 端口没人监听;或者防火墙规则写的是 reject |
# firewalld 挡住时(CentOS)
curl: (7) Failed to connect to 192.168.3.101 port 8080 after 30 ms: No route to host
# ufw 挡住时(Ubuntu)
curl: (28) Connection timed out after 4001 milliseconds
No route to host 很容易被当成网络不通去查路由,其实是 firewalld 用 ICMP 回了一个「主机禁止」。
2.2 确认用的是哪套防火墙
# systemctl is-active firewalld; firewall-cmd –state # CentOS / Rocky
active
running
# ufw status # Ubuntu
Status: inactive
⚠️ Ubuntu 上 systemctl is-active ufw 会显示 active,但 ufw status 是 Status: inactive —— ufw 有没有生效看 ufw status,别看 systemctl。
2.3 firewalld(CentOS 7 / Rocky 9)
看当前规则:
# firewall-cmd –get-default-zone; firewall-cmd –get-active-zones
public
public
interfaces: ens33
# firewall-cmd –list-all (Rocky)
public (active)
target: default
interfaces: enp5s0
services: cockpit dhcpv6-client ssh
ports:
…
开一个端口,临时和永久是两件事:
# firewall-cmd –add-port=8080/tcp # 只改运行时,立即生效
success
# firewall-cmd –reload; firewall-cmd –list-ports
success
# reload 之后没了
# firewall-cmd –permanent –add-port=8080/tcp # 只写配置,运行时还没有
success
# firewall-cmd –list-ports
# 空的
# firewall-cmd –reload; firewall-cmd –list-ports
success
8080/tcp # reload 之后才生效
✅ 标准写法:–permanent 加完再 –reload。或者先临时加、确认好用后 firewall-cmd –runtime-to-permanent 把运行时规则存成永久。
其它常用操作(两台都测过):
firewall-cmd –permanent –add-port=9000-9010/tcp # 端口段用减号
firewall-cmd –permanent –add-port=5353/udp
firewall-cmd –permanent –add-service=http –add-service=https
firewall-cmd –info-service=mysql # 看一个服务名对应哪些端口 → ports: 3306/tcp
firewall-cmd –query-port=3306/tcp; echo $? # 没开:输出 no,退出码 1
firewall-cmd –permanent –remove-port=8081/tcp # 删端口
firewall-cmd –add-port=7777/tcp –timeout=5s # 5 秒后自动删掉(实测 7 秒后已不在列表里)
已经开了的服务再加一次会有警告,不影响:Warning: ALREADY_ENABLED: http。
只允许某个 IP 访问某个端口(富规则):
# firewall-cmd –permanent –add-rich-rule='rule family="ipv4" source address="192.168.3.84/32" port port="8080" protocol="tcp" accept'
# firewall-cmd –reload; firewall-cmd –list-rich-rules
rule family="ipv4" source address="192.168.3.84/32" port port="8080" protocol="tcp" accept
从 192.168.3.84 访问能通,其它机器不通。拒绝某个 IP:
# firewall-cmd –permanent –add-rich-rule='rule family="ipv4" source address="192.168.3.84" reject'
被 reject 的一方看到的是 Connection refused,不是 No route to host。
信任某个 IP 的所有访问(放进 trusted 区):
# firewall-cmd –permanent –zone=trusted –add-source=192.168.3.84; firewall-cmd –reload
# firewall-cmd –get-active-zones
public
interfaces: ens33
trusted
sources: 192.168.3.84
端口转发(外面访问 18080,转到本机 8080):
# firewall-cmd –permanent –add-forward-port=port=18080:proto=tcp:toport=8080; firewall-cmd –reload
# firewall-cmd –list-forward-ports
port=18080:proto=tcp:toport=8080:toaddr=
从另一台 curl http://IP:18080/ 拿到了 8080 上的页面。
永久规则存在 /etc/firewalld/zones/public.xml:
<zone>
<short>Public</short>
…
<service name="ssh"/>
<port protocol="tcp" port="9000-9010"/>
<port protocol="udp" port="5353"/>
</zone>
🔴 CentOS 7 上 –runtime-to-permanent 会改网卡配置文件。我这台执行完之后,/etc/sysconfig/network-scripts/ifcfg-ens33 末尾多了一行 ZONE=public,文件权限从 644 变成 600。我把文件还原后单独复现了一次:–reload、–permanent –add-port 都不会动它,只有 –runtime-to-permanent 会:
# firewall-cmd –runtime-to-permanent; stat -c '%a %s' …/ifcfg-ens33; grep -n ZONE …/ifcfg-ens33
success
600 322
16:ZONE=public
功能上没影响(网卡本来就在 public 区),但如果你用配置管理工具比对 ifcfg 文件,会看到一处莫名其妙的改动。
临时关掉防火墙(排查用,之后记得开回来):
systemctl stop firewalld # 停了之后,从外面 curl 8081 立刻通了
systemctl start firewalld
2.4 ufw(Ubuntu 24.04)
ufw 默认没启用。🔴 启用前先放行 SSH,否则你可能把自己关在外面:
# ufw allow OpenSSH # 或 ufw allow 22/tcp
Rules updated
Rules updated (v6)
# ufw –force enable
Firewall is active and enabled on system startup
# ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
…
⚠️ 不加 –force 时,ufw enable 会问 Command may disrupt existing ssh connections. Proceed with operation (y|n)?。在脚本里读不到 y 时它打印 Aborted,退出码却是 0,脚本会以为已经启用了:
# ufw enable < /dev/null; echo "rc=$?"; ufw status
Command may disrupt existing ssh connections. Proceed with operation (y|n)? Aborted
rc=0
Status: inactive
开端口:
ufw allow 8080/tcp
ufw allow 5353/udp
ufw allow 80,443/tcp
ufw allow 9000:9010/tcp # 端口段用冒号
🔴 端口段写成减号会报错(firewalld 正好相反,用减号):
# ufw allow 9000-9010/tcp
ERROR: Bad port
只允许某个 IP 访问某个端口:
# ufw allow from 10.115.180.1 to any port 8081 proto tcp
看规则、按编号删:
# ufw status numbered
To Action From
— —— —-
[ 1] OpenSSH ALLOW IN Anywhere
[ 2] 22/tcp ALLOW IN Anywhere
[ 3] 8080/tcp ALLOW IN Anywhere
…
# ufw –force delete 3 # 不加 –force 会问确认,脚本里同样是 Aborted + 退出码 0
# ufw delete allow 8080/tcp # 按规则内容删,IPv4 和 IPv6 两条一起删
Rule deleted
Rule deleted (v6)
🔴 规则有顺序,先匹配先生效。已经放行了 8080,再 ufw deny from 10.115.180.1,这条 deny 排在后面,那个 IP 照样能访问 8080。要让它生效得插到最前面:
# ufw insert 1 deny from 10.115.180.1
Rule inserted
插到第 1 条之后,那个 IP 访问 8080 就超时了。
deny 是直接丢包(对方超时),reject 会回一个拒绝(对方立刻看到 Connection refused):
# ufw insert 1 reject from 10.115.180.1 to any port 8080 proto tcp
防 SSH 暴力破解:ufw limit 22/tcp,会把已有的 allow 22/tcp 改成 LIMIT(连载第二篇测过:30 秒内第 6 次连接被拒)。
其它:
ufw app list # 应用配置:OpenSSH 等
ufw logging on # 日志写到 /var/log/syslog、/var/log/kern.log,关键字 UFW
ufw reload
ufw disable # 停用;注意 systemctl is-enabled ufw 仍显示 enabled
ufw –force reset # 清空所有规则,会自动把旧规则备份成 /etc/ufw/user.rules.<时间戳>
ufw 的规则最终落在 iptables(nftables 后端)里,iptables -S | grep -c ufw 能数出上百条 ufw 链。
2.5 iptables 与规则持久化
Rocky 9:换成 iptables-services(不用 firewalld 的做法):
# dnf install -y iptables-services
# systemctl disable –now firewalld; systemctl enable –now iptables
# iptables -L INPUT -n –line-numbers
num target prot opt source destination
1 ACCEPT all — 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
2 ACCEPT icmp — 0.0.0.0/0 0.0.0.0/0
3 ACCEPT all — 0.0.0.0/0 0.0.0.0/0
4 ACCEPT tcp — 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22
5 REJECT all — 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited
⚠️ 在 Rocky 9 上,dnf install iptables-services 实际装上的包叫 iptables-nft-services,所以之后用 rpm -q iptables-services 查会显示 package iptables-services is not installed,别被它骗了。
加规则要插在 REJECT 之前(-A 追加到最后,永远轮不到;连载第二篇有实测):
# iptables -I INPUT 5 -p tcp –dport 8080 -j ACCEPT # 插到第 5 条
# iptables -I INPUT -p tcp -s 10.115.180.0/24 –dport 3306 -j ACCEPT # 插到最前面
# service iptables save
iptables: Saving firewall rules to /etc/sysconfig/iptables: [ OK ]
备份和恢复、删除:
iptables-save > /root/ipt.bak
iptables-restore < /root/ipt.bak
iptables -D INPUT -p tcp –dport 8080 -j ACCEPT # 按规则内容删
iptables -D INPUT 1 # 按编号删
iptables -L INPUT -n -v # -v 看每条规则命中了多少包
Ubuntu:iptables-persistent
# apt install -y iptables-persistent
# iptables -I INPUT -p tcp –dport 8080 -j ACCEPT
# netfilter-persistent save
# grep 8080 /etc/iptables/rules.v4
-A INPUT -p tcp -m tcp –dport 8080 -j ACCEPT
🔴 在 Ubuntu 24.04 上装 iptables-persistent 会把 ufw 卸掉(ufw 声明了 Breaks: iptables-persistent),而且之后卸掉 iptables-persistent,ufw 也不会自己回来:
# dpkg -s ufw | grep ^Status
Status: deinstall ok config-files
两者选一个用,别混用。
(如果 apt 报 E: Package 'iptables-persistent' has no installation candidate,先 apt update —— 我这台云镜像恢复快照后软件源索引是空的,就是这个报错。)
2.6 firewalld 和手工 iptables 规则混用
连载第二篇专门测过:CentOS 7 上手工加的 iptables 规则,firewall-cmd –reload 一次就没了;Rocky 9 上反而留着,而且 iptables -L 看不到 firewalld 的规则。用 firewalld 的机器,就只用 firewall-cmd 改规则。
2.7 防火墙之外:云安全组、SELinux、监听地址
端口开了还是不通,继续查这三处:
- 云服务器的安全组(在云厂商控制台,机器里看不到);
- SELinux:服务换了非标准端口,要 semanage port -a(第 17 章);
- 服务只监听了 127.0.0.1:ss -lntp 看 Local Address,是 127.0.0.1:8080 就只有本机能连(第 9 章)。
3. 装软件:yum / dnf / apt
3.1 三个系统的对照
| 刷新软件源索引 | yum makecache | dnf makecache | apt update |
| 搜索 | yum search tree | dnf search tree | apt search '^tree$' |
| 看详情 | yum info tree | dnf info tree | apt show tree |
| 安装 | yum install -y tree | dnf install -y tree | apt install -y tree |
| 卸载 | yum remove -y tree | dnf remove -y tree | apt remove / apt purge(连配置删) |
| 已装列表 | rpm -qa / yum list installed | 同左 | dpkg -l / apt list –installed |
| 哪些能升级 | yum check-update | dnf check-update | apt list –upgradable |
| 某文件属于哪个包 | rpm -qf /usr/bin/tree | 同左 | dpkg -S /usr/bin/tree |
| 包里有哪些文件 | rpm -ql tree | 同左 | dpkg -L tree |
| 命令在哪个包里(没装) | yum provides '*/bin/netstat' | dnf provides netstat | apt-file search bin/netstat |
| 历史与撤销 | yum history / yum history undo last | 同左(dnf) | 只有日志 /var/log/apt/history.log,没有 undo |
Rocky 9 上 yum 就是 dnf,两个名字都能用。
3.2 装一个包,看看它装了什么(三台实测)
# yum install -y tree (CentOS,截取末尾)
Installed:
tree.x86_64 0:1.6.0-10.el7
Complete!
# rpm -qf /usr/bin/tree
tree-1.6.0-10.el7.x86_64
# rpm -ql tree
/usr/bin/tree
/usr/share/doc/tree-1.6.0
/usr/share/doc/tree-1.6.0/LICENSE
/usr/share/doc/tree-1.6.0/README
/usr/share/man/man1/tree.1.gz
# rpm -qc openssh-server # 只看配置文件
/etc/pam.d/sshd
/etc/ssh/sshd_config
/etc/sysconfig/sshd
同一个 tree:CentOS 7 是 1.6.0,Rocky 9 是 1.8.0,Ubuntu 24.04 是 2.1.1。
# dpkg -S /usr/bin/tree (Ubuntu)
tree: /usr/bin/tree
# dpkg -s tree | grep -E '^(Status|Version)'
Status: install ok installed
Version: 2.1.1-2ubuntu3.24.04.2
3.3 Ubuntu:先 apt update 再装
新装的系统或者刚恢复的快照,软件源索引可能是空的,直接装会报「找不到包」:
# apt install -y tree
E: Unable to locate package tree
# apt update (截取末尾)
7 packages can be upgraded. Run 'apt list –upgradable' to see them.
# apt install -y tree ← 这次就装上了
⚠️ 在脚本里用 apt 会打出 WARNING: apt does not have a stable CLI interface。脚本里用 apt-get,交互时用 apt。
3.4 某个命令没有,该装哪个包?
RHEL 系用 provides。🔴 CentOS 7 的 yum provides 必须带路径通配,Rocky 9 的 dnf 不用:
# yum provides netstat (CentOS 7)
No matches found
# yum provides '*/bin/netstat' ← 这样才查得到
# dnf provides netstat (Rocky 9)
net-tools-2.0-0.64.20160912git.el9.x86_64 : Basic networking tools
Repo : baseos
Ubuntu 用 apt-file:
# apt install -y apt-file; apt-file update
# apt-file search bin/netstat
net-tools: /bin/netstat
netstat-nat: /usr/bin/netstat-nat
下面这张表是我在三台上逐个装过、确认命令能用的结果。「—」表示没找到对应的包。
| netstat ifconfig route arp | net-tools | net-tools | net-tools |
| dig nslookup | bind-utils | bind-utils | bind9-dnsutils |
| host | bind-utils | bind-utils | bind9-host |
| nc | nmap-ncat | nmap-ncat | netcat-openbsd |
| telnet traceroute tcpdump nmap whois iperf3 | 同名 | 同名 | 同名 |
| tracepath | iputils | iputils | iputils-tracepath |
| mtr | mtr | mtr | mtr-tiny |
| iostat sar mpstat | sysstat | sysstat | sysstat |
| killall pstree fuser | psmisc | psmisc | psmisc |
| lsof strace iotop | 同名 | 同名 | 同名 |
| htop iftop nethogs ncdu | 同名(EPEL) | 同名(EPEL) | 同名 |
| tree wget rsync zip unzip bzip2 tar zstd | 同名 | 同名 | 同名 |
| 7za | p7zip(EPEL) | p7zip(EPEL) | p7zip-full |
| unrar | — | unrar(EPEL) | unrar |
| vim | vim-enhanced | vim-enhanced | vim |
| setfacl getfacl | acl | acl | acl |
| locate | mlocate | mlocate | plocate |
| rename | util-linux(自带) | util-linux(自带) | rename(Perl 版,语法不同) |
| lsb_release | redhat-lsb-core | lsb_release(EPEL) | lsb-release |
| semanage | policycoreutils-python | policycoreutils-python-utils | policycoreutils-python-utils |
| sealert | setroubleshoot-server | setroubleshoot-server | — |
| rz sz | lrzsz | lrzsz | lrzsz |
| ab | httpd-tools | httpd-tools | apache2-utils |
| mailx | s-nail | bsd-mailx | |
| ntpdate | ntpdate | —(用 chrony) | ntpsec-ntpdate |
| chronyc | chrony | chrony | chrony |
| crontab | cronie | cronie | cron |
| man | man-db | man-db | man-db |
| pip3 | python3-pip | python3-pip | python3-pip |
| file bc at screen tmux git jq expect sshpass dos2unix pv gcc make nano | 同名 | 同名(部分在 EPEL) | 同名 |
Rocky 9 上标了 EPEL 的,先 dnf install -y epel-release(见 3.8)。
3.5 下载 rpm / deb,离线装、本地装
# dnf download tree (CentOS 7 用 yumdownloader tree)
tree-1.8.0-10.el9.x86_64.rpm 311 kB/s | 55 kB 00:00
# yum install -y –downloadonly –downloaddir=/tmp/dl telnet # 连依赖一起下载,不安装
# ls /tmp/dl
telnet-0.17-66.el7.x86_64.rpm
# apt download tree (Ubuntu)
Fetched 47.4 kB in 1s (54.9 kB/s)
本地包用包管理器装,它会自动解决依赖:
dnf install -y ./tree-1.8.0-10.el9.x86_64.rpm # CentOS 7 用 yum install
apt install -y ./tree_2.1.1-2ubuntu3.24.04.2_amd64.deb # 注意要写 ./
apt 装本地文件时不写 ./,会被当成包名去源里找:
# apt install -y tree_2.1.1-2ubuntu3.24.04.2_amd64.deb
E: Unable to locate package tree_2.1.1-2ubuntu3.24.04.2_amd64.deb
rpm -ivh、dpkg -i 也能装,但不解依赖(连载第一篇有缺依赖时的报错)。同一个包已经装了再 rpm -Uvh,报 package tree-… is already installed。
不装,只看包里的内容:
rpm -qp –requires ./tree-*.rpm # 依赖
rpm -qpl ./tree-*.rpm # 文件列表
dpkg -I ./tree_*.deb # 包信息(Package / Version / Depends)
dpkg -c ./tree_*.deb # 文件列表
3.6 指定版本、锁定版本、排除升级
# yum –showduplicates list openssl | tail -3 (CentOS 7)
openssl.x86_64 1:1.0.2k-24.el7_9 updates
openssl.x86_64 1:1.0.2k-25.el7_9 updates
openssl.x86_64 1:1.0.2k-26.el7_9 updates
# apt-cache madison openssl (Ubuntu)
openssl | 3.0.13-0ubuntu3.15 | http://archive.ubuntu.com/ubuntu noble-updates/main amd64 Packages
openssl | 3.0.13-0ubuntu3 | http://archive.ubuntu.com/ubuntu noble/main amd64 Packages
装指定版本:RHEL 系写成「包名-版本」,Ubuntu 写成「包名=版本」:
# rpm -q tzdata; dnf install -y tzdata-2026b-1.el9_8; rpm -q tzdata (Rocky 9)
tzdata-2026c-1.el9_8.noarch
…
tzdata-2026b-1.el9_8.noarch ← 指定的版本比已装的旧,dnf install 直接降级
# apt install -y tree=2.1.1-2ubuntu3; dpkg -l tree | tail -1 (Ubuntu)
ii tree 2.1.1-2ubuntu3 amd64 displays an indented directory tree, in color
锁版本(三台都测过):
# CentOS 7
yum install -y yum-plugin-versionlock
yum versionlock add tree # → Adding versionlock on: 0:tree-1.6.0-10.el7
yum versionlock list
yum versionlock delete tree
# Rocky 9
dnf install -y 'dnf-command(versionlock)'
dnf versionlock add tree # → Adding versionlock on: tree-0:1.8.0-10.el9.*
# Ubuntu
apt-mark hold tree # → tree set on hold.
apt-mark showhold
apt-mark unhold tree
排除某类包不升级(RHEL 系):在 /etc/yum.conf 里加 exclude=kernel*,之后 yum check-update 'kernel*' 就看不到内核更新;临时解除用 –disableexcludes=all。
3.7 历史与撤销(RHEL 系)
# dnf history | head -4
ID | Command line | Date and time | Action(s) | Altered
——————————————————————————-
10 | remove -y tree | 2026-09-22 11:52 | Removed | 1
9 | install -y tree | 2026-09-22 11:52 | Install | 1
# dnf history undo last -y ← 撤销上一次(这里等于把 tree 装回来)
# dnf history info last | grep -E 'Command|Install'
Command Line : history undo last -y
Install tree-1.8.0-10.el9.x86_64 @baseos
Ubuntu 没有 undo,只能看日志:grep -E '^(Start-Date|Commandline)' /var/log/apt/history.log。
3.8 EPEL、CRB、模块流(Rocky 9)
# dnf install -y epel-release
# dnf repolist
repo id repo name
appstream Rocky Linux 9 – AppStream
baseos Rocky Linux 9 – BaseOS
epel Extra Packages for Enterprise Linux 9 – x86_64
epel-cisco-openh264 Extra Packages for Enterprise Linux 9 openh264 (From Cisco) – x86_64
extras Rocky Linux 9 – Extras
# dnf config-manager –set-enabled crb # CentOS 8 时代叫 powertools,9 上叫 crb
# dnf module list nginx
Name Stream Profiles Summary
nginx 1.22 common [d] nginx webserver
nginx 1.24 common [d] nginx webserver
nginx 1.26 common [d] nginx webserver
CentOS 7 的 EPEL 已经下线,要改用归档地址(3.10)。
3.9 卸载干净(Ubuntu)
apt remove -y 包名 # 删程序,留配置文件
apt purge -y 包名 # 连配置一起删
apt autoremove -y # 删掉当初作为依赖装进来、现在没人用的包
apt remove 之后 /etc/nginx 还在,是连载第一篇专门测过的坑。另外,–no-install-recommends 可以少装推荐包;apt-get -s install 包名 只模拟、不真装。
我这台 Ubuntu 的 unattended-upgrades 是开着的(/etc/apt/apt.conf.d/20auto-upgrades 里两行都是 "1"),会自动装安全更新。
3.10 换国内源(三台实测,细节见连载第一篇)
Rocky 9 → 阿里云:
cp -a /etc/yum.repos.d /root/yum.repos.d.bak
sed -e 's|^mirrorlist=|#mirrorlist=|g' \\
-e 's|^#baseurl=http://dl.rockylinux.org/$contentdir|baseurl=https://mirrors.aliyun.com/rockylinux|g' \\
-i.bak /etc/yum.repos.d/rocky*.repo
dnf clean all && dnf makecache
# dnf repoinfo baseos | grep Repo-baseurl
Repo-baseurl : https://mirrors.aliyun.com/rockylinux/9/BaseOS/x86_64/os/
Ubuntu 24.04 → 阿里云。🔴 源已经不在 /etc/apt/sources.list 里了,改的是 ubuntu.sources,而且 archive 和 security 两个地址都要改:
cp /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.bak
sed -i 's|//archive.ubuntu.com|//mirrors.aliyun.com|g' /etc/apt/sources.list.d/ubuntu.sources
sed -i 's|//security.ubuntu.com|//mirrors.aliyun.com|g' /etc/apt/sources.list.d/ubuntu.sources
apt update
CentOS 7:官方源已经下线,切到阿里云归档 centos-vault,baseurl 写死 7.9.2009(不能用 $releasever,它展开成 7,归档站没有这个目录):
[base]
name=CentOS-7 – Base
baseurl=https://mirrors.aliyun.com/centos-vault/7.9.2009/os/$basearch/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7
(updates、extras 两段同理,把 os 换成对应目录。)EPEL 7 同样要换成归档:baseurl=https://mirrors.aliyun.com/epel-archive/7/$basearch。
3.11 一个坏掉的源,会拖慢所有 yum 命令
我这台 CentOS 上有一个 SCLo 源在两个文件里重复定义,其中一个地址返回 403。结果每条 yum 命令都先刷一屏错误,makecache 要 1 分 35 秒:
Repository centos-sclo-sclo is listed more than once in the configuration
https://mirrors.tuna.tsinghua.edu.cn/centos/7/sclo/x86_64/sclo/repodata/repomd.xml: [Errno 14] HTTPS Error 403 – Forbidden
临时跳过它:
# time yum –disablerepo='centos-sclo*' makecache
Metadata Cache Created
real0m0.162s
长期的办法是把那个 .repo 里的段改成 enabled=0,或者删掉重复的那份。yum repolist all | grep -i sclo 能看到它们各自的状态。
4. 基础命令
这一章三个系统几乎一样,只列有差异的地方和容易用错的地方。
4.1 我是谁、在哪、机器怎么样
whoami; id; hostname; hostname -I; date; uptime; pwd
# id (CentOS,SELinux 开着所以有 context)
uid=0(root) gid=0(root) groups=0(root) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
# uptime
19:54:08 up 43 min, 3 users, load average: 0.26, 0.11, 0.06
cd – 回到上一个目录并打印它,cd ~ 回家目录,cd .. 上一级。
4.2 ls 常用写法
ls -l # 长格式
ls -la # 含隐藏文件
ls -lh # 大小带单位:-rwxr-xr-x. 1 root root 942K Nov 25 2021 /usr/bin/bash
ls -ld /etc # 看目录本身,不看目录里面
ls -lt # 按修改时间,新的在前
ls -ltr # 按修改时间,旧的在前(最新的在最后一行,看日志目录很方便)
ls -lS # 按大小
ls -i # 显示 inode 号
ls –full-time # 完整时间:-rw-r–r–. 1 root root 158 2013-06-07 22:31:32.000000000 +0800 /etc/hosts
⚠️ ll 在三个系统上含义不一样:CentOS/Rocky 在 /etc/profile.d/colorls.sh 里定义成 ls -l,Ubuntu 在 ~/.bashrc 里定义成 ls -alF(带隐藏文件)。脚本里别用 ll。
4.3 看文件内容
cat -n 文件 # 带行号
head -n 3 文件 # 前 3 行
tail -n 2 文件 # 最后 2 行
tail -n +30 文件 # 从第 30 行到结尾
sed -n '2,3p' 文件 # 第 2~3 行
less 文件 # 分页看,q 退出
wc -l 文件 # 行数
file 文件 # 判断文件类型:ASCII text / ELF 64-bit LSB executable / OpenSSH RSA public key
stat 文件 # 权限、属主、三个时间、inode
4.4 命令在哪、是什么
# type cd ls; type -a echo
cd is a shell builtin
ls is /usr/bin/ls
echo is a shell builtin
echo is /usr/bin/echo
# which ls; whereis ls
/usr/bin/ls
ls: /usr/bin/ls /usr/share/man/man1/ls.1.gz
# command -v nosuchcmd; echo "rc=$?"
rc=1 ← 脚本里判断命令是否存在,用 command -v
4.5 帮助
help cd # shell 内建命令的帮助
ls –help # 大多数命令都有
man ls # 手册页(我这台 Ubuntu 云镜像没装 man,apt install man-db)
man -f crontab # 同名手册有几份:crontab (1) 是命令,crontab (5) 是文件格式
man -k 'list directory'
4.6 环境变量
# export MYVAR=hello; bash -c 'echo "子shell: $MYVAR"'
子shell: hello
# MYVAR2=world; bash -c 'echo "未export: [$MYVAR2]"'
未export: [] ← 不 export,子进程看不到
echo $PATH 看命令搜索路径。三台的默认 PATH 不同,我这台 CentOS 因为启用了 devtoolset,PATH 最前面是 /opt/rh/devtoolset-7/root/usr/bin(第 10 章会讲它带来的一个坑)。
LANG:我这台 CentOS 的系统 locale 是 zh_CN.UTF-8(普通用户看到的报错是中文);两台云镜像是 C.utf8 / en_US.utf8。
4.7 重定向
# ls /nosuch > /tmp/o.txt 2> /tmp/e.txt; echo "rc=$?"
rc=2 ← 标准输出和错误分开存
# ls /etc/hosts /nosuch > /tmp/all.txt 2>&1 ← 都存到一个文件
# ls /etc/hosts /nosuch &> /tmp/all2.txt ← 同上的简写(bash)
# echo a > /tmp/x; echo b >> /tmp/x; cat /tmp/x
a
b ← > 覆盖,>> 追加
# ls /nosuch 2>/dev/null ← 丢掉错误
2>&1 要写在 > 后面,写在前面不生效;sudo echo x > /etc/文件 会权限不够,要用 echo x | sudo tee /etc/文件 —— 这两个都是连载第九篇的实测。
同时输出到屏幕和文件用 tee,追加用 tee -a:
# echo hi | tee /tmp/t1 | tr a-z A-Z
HI
here document:结束符加引号,里面的变量就不展开:
# cat <<EOT # 展开:line1 /root
line1 $HOME
EOT
# cat <<'EOT' # 不展开:no expand $HOME
no expand $HOME
EOT
4.8 通配符与大括号
ls /etc/*.conf # 任意字符
ls -d /etc/host? # 单个字符 → /etc/hosts
echo /etc/{hosts,passwd} # → /etc/hosts /etc/passwd
mkdir -p /tmp/w/{a,b,c}/x # 一次建三个目录
echo {1..5}; echo {01..03} # → 1 2 3 4 5 / 01 02 03
通配符匹配不到时,原样传给命令:ls: cannot access '/tmp/nomatch*.xyz': No such file or directory。加引号就不展开:echo '/etc/pass*' 输出的就是 /etc/pass*。
文件名有空格,要么加引号,要么反斜杠转义:ls -l "/tmp/a b" 等价于 ls -l /tmp/a\\ b。
4.9 命令组合
cmd1 && cmd2 # cmd1 成功才执行 cmd2
cmd1 || cmd2 # cmd1 失败才执行 cmd2
cmd1; cmd2 # 不管成败都执行
(cd /; pwd); pwd # 小括号里的 cd 不影响外面
echo $? # 上一条命令的退出码,0 是成功
n=$(ls /etc | wc -l) # 命令替换
echo $((7/2)) # → 3,整数运算
小数运算:echo "scale=2;3/2" | bc → 1.50(bc 在我这台 CentOS 上有,两台云镜像没有);或者 python3 -c 'print(3/2)' → 1.5。我这台 CentOS 只有 Python 2.7,没有 python3;Rocky 是 3.9.25,Ubuntu 是 3.12.3,两台都没有 python 这个命令名。
4.10 日期
date +"%Y-%m-%d %H:%M:%S" # 2026-09-22 19:54:09
date +%s # 时间戳 1790078049
date -d @0 # 时间戳转日期:Thu Jan 1 08:00:00 CST 1970(CentOS 时区是东八区)
date -d "yesterday" +%F # 2026-09-21
date -d "+3 days" +%F # 2026-09-25
date -d "2026-01-01" +%A # Thursday
时区设置见第 18 章。
4.11 xargs
# seq 3 | xargs -I{} echo item-{}
item-1
item-2
item-3
# find /etc/skel -type f | xargs ls -l | wc -l
3
文件名里有空格时用 find … -print0 | xargs -0(第 5 章)。
5. 文件与目录
5.1 创建
# mkdir a; mkdir a; echo "rc=$?"
mkdir: cannot create directory ‘a’: File exists
rc=1
# mkdir -p a/b/c && mkdir -p a/b/c && echo "-p 已存在不报错"
-p 已存在不报错
# mkdir -m 700 priv; ls -ld priv
drwx——. 2 root root 6 Sep 22 19:54 priv
# touch -d "2020-01-01 10:00" old # 建文件并指定时间
# : > empty # 建空文件 / 清空已有文件
5.2 复制:cp
cp f1 f1.bak # 复制文件
cp f1 a/ # 复制到目录里
cp -r a a2 # 复制目录,必须 -r
cp -a src dst # 保留权限、属主、时间、软链(备份用这个)
cp -p f g # 保留权限和时间
cp -v f1 f4 # 显示过程:'f1' -> 'f4'
cp –backup=numbered f1 f4 # 目标已存在时先备份成 f4.~1~、f4.~2~
复制目录忘了 -r:
cp: omitting directory ‘a’ # CentOS 7
cp: -r not specified; omitting directory 'a' # Rocky 9 / Ubuntu 24.04
默认 cp 会把时间改成现在:
-rw-r–r–. 1 root root 0 2020-01-01 10:00 old ← 源文件
-rw-r–r–. 1 root root 0 2026-09-22 19:54 old.cp ← cp
-rw-r–r–. 1 root root 0 2020-01-01 10:00 old.p ← cp -p
-rw-r–r–. 1 root root 0 2020-01-01 10:00 old.a ← cp -a
⚠️ cp -r a a2 执行两次,第二次会得到 a2/a(嵌套了一层)。只想复制目录里的内容,写 cp -r a/. a3/。
⚠️ 目标已存在时 cp -i 会问 cp: overwrite 'f2'?;在脚本里读不到回答就不覆盖。不想覆盖也可以用 cp -n,但 Ubuntu 24.04 的 coreutils 会提示 cp: warning: behavior of -n is non-portable and may change in future; use –update=none instead。
5.3 移动与重命名:mv
mv f4 f5 # 改名
mv f5 a/ # 移到目录
mv -n f1 f2 # 目标存在就不动
mv -i f1 f2 # 目标存在先问
批量改后缀:CentOS 和 Rocky 的 rename 来自 util-linux,语法是「旧 新 文件」:
# touch r1.txt r2.txt; rename .txt .log r*.txt; ls r*
r1.log
r2.log
Ubuntu 上 rename 要另装,而且装的是 Perl 版,语法不一样。三个系统都能用的写法:
for f in *.log; do mv — "$f" "${f%.*}.md"; done
5.4 删除:rm
rm 文件
rm -r 目录 # 删目录
rm -f 不存在的文件; echo $? # → 0,-f 不报错
rm 不存在的文件; echo $? # → rm: cannot remove 'nosuch': No such file or directory,1
rmdir 目录 # 只能删空目录:rmdir: failed to remove 'a': Directory not empty
rm -i 文件 # 删前确认
文件名以减号开头:
# rm -rf.txt
rm: invalid option — '.'
Try 'rm ./-rf.txt' to remove the file '-rf.txt'.
# rm — -rf.txt ← 或 rm ./-rf.txt
rm -rf "$DIR"/ 在变量为空时会变成 rm -rf /,写脚本时用 rm -rf "${DIR:?}"/(连载第七篇有实测)。
5.5 权限
chmod 755 p # -rwxr-xr-x
chmod u=rw,g=r,o= p # -rw-r—–
chmod +x p # 所有人加执行
chmod o-x,g+w p
stat -c '%a %A %n' p # → 660 -rw-rw—- p,数字和字母对照
🔴 目录和文件分开设,别 chmod -R 755 一把梭(会把普通文件也变成可执行):
find d -type d -exec chmod 755 {} +
find d -type f -exec chmod 644 {} +
umask 决定新建文件的默认权限:
# umask; umask -S
0022
u=rwx,g=rx,o=rx
# touch um1; mkdir umd; ls -ld um1 umd
-rw-r–r–. 1 root root 0 Sep 22 19:54 um1
drwxr-xr-x. 2 root root 6 Sep 22 19:54 umd
# (umask 077; touch um2; mkdir umd2; ls -ld um2 umd2)
-rw——-. 1 root root 0 Sep 22 19:54 um2
drwx——. 2 root root 6 Sep 22 19:54 umd2
特殊权限位:
# ls -ld /tmp /usr/bin/passwd
drwxrwxrwt. 41 root root 8192 Sep 22 19:54 /tmp ← t:粘滞位,只能删自己的文件
-rwsr-xr-x. 1 root root 27856 Apr 1 2020 /usr/bin/passwd ← s:SUID,以属主身份运行
# mkdir sg; chmod g+s sg; ls -ld sg
drwxr-sr-x. 2 root root 6 Sep 22 19:54 sg ← SGID 目录:里面新建的文件继承目录的组
SGID 共享目录「设了还是不能一起改」是连载第七篇的一节。
5.6 属主与属组
chown nobody own # 改属主
chown nobody:nobody own # 同时改组(Ubuntu 上 nobody 的组叫 nogroup)
chown :root own # 只改组
chgrp nobody own
chown -R root:root d # 递归
# chown nosuchuser own; echo "rc=$?"
chown: invalid user: ‘nosuchuser’
rc=1
nobody 的 UID:CentOS 7 是 99,Rocky 9 和 Ubuntu 是 65534。
5.7 ACL:给某个用户单独开权限
# setfacl -m u:nobody:rw acl; getfacl acl
# file: acl
# owner: root
# group: root
user::rw-
user:nobody:rw-
group::r–
mask::rw-
other::r–
# ls -l acl
-rw-rw-r–+ 1 root root 0 Sep 22 19:54 acl ← 末尾的 + 表示有 ACL
# setfacl -b acl ← 清掉所有 ACL
我这台 Ubuntu 云镜像没有 setfacl,要 apt install acl。
5.8 链接
# echo s > src; ln src hard; ln -s src soft; ls -li src hard soft
34232478 -rw-r–r–. 2 root root 2 Sep 22 19:54 hard
34232479 lrwxrwxrwx. 1 root root 3 Sep 22 19:54 soft -> src
34232478 -rw-r–r–. 2 root root 2 Sep 22 19:54 src
# rm src; cat hard; cat soft
s
cat: soft: No such file or directory ← 硬链接还能读,软链接断了
# readlink -f soft ← 看软链最终指向
# find . -xtype l ← 找出断掉的软链
🔴 换一个指向目录的软链,要加 -n:
# ln -s t1 cur; ln -sf t2 cur; ls -l cur; ls t1
lrwxrwxrwx. 1 root root 2 Sep 22 19:54 cur -> t1 ← 没换掉
t2 ← 反而在 t1 里面建了一个 t2
# ln -sfn t2 cur; ls -l cur
lrwxrwxrwx. 1 root root 2 Sep 22 19:54 cur -> t2
5.9 防误删:chattr
# chattr +i lock; lsattr lock; rm -f lock
—-i———– lock
rm: cannot remove 'lock': Operation not permitted ← root 也删不掉
# chattr -i lock ← 去掉才能删
# chattr +a alog; echo x >> alog; echo y > alog
bash: alog: Operation not permitted ← +a 只能追加,不能覆盖
5.10 查找:find
find /etc -name "hosts" # 按名字
find /etc -iname "HOSTS" # 忽略大小写
find /etc -maxdepth 1 -name "*.conf" # 只找一层
find /etc -type d -name "ssh" # 只找目录
find /var/log -type f -size +100k # 大于 100k
find /etc -type f -mtime -1 # 1 天内改过
find /etc -type f -newer /etc/hostname # 比某个文件新
find /var/log -name "*.log" -mtime +30 # 30 天前的日志
find /tmp/lab5 -empty # 空文件和空目录
find / -xdev -type f -perm -4000 # SUID 文件,-xdev 不跨文件系统
find / -xdev -nouser # 属主已经不存在的文件(删用户后留下的)
找到后处理:
find . -name "*.tmp" -delete # 直接删
find . -name "*.tmp" -exec ls -l {} \\; # 每个文件执行一次
find . -name "*.tmp" -print0 | xargs -0 rm -v # 文件名有空格也不怕
🔴 -name 后面的通配符一定加引号。不加引号时,如果当前目录恰好有 .conf 文件,shell 会先把 *.conf 展开成那个文件名,结果什么都搜不到:
# cd /tmp; touch zz.conf; find /etc -name *.conf
← 空的:实际执行的是 find /etc -name zz.conf
locate 更快,但三台都要另装(CentOS/Rocky 是 mlocate,Ubuntu 是 plocate)。
5.11 目录占用、路径处理
du -sh /etc /var/log # 36M /etc,43M /var/log
du -h –max-depth=1 /var | sort -h | tail # 哪个子目录最大
ls /etc | wc -l # 一级条目数
find /etc -type f | wc -l # 文件总数
basename /etc/ssh/sshd_config # sshd_config
basename /tmp/a.tar.gz .gz # a.tar
dirname /etc/ssh/sshd_config # /etc/ssh
realpath /tmp/lab5/../lab5 # /tmp/lab5
mktemp; mktemp -d # 建临时文件/目录:/tmp/tmp.jEBCSts8k3
6. 文本处理
本章的例子都用下面两份文件(三台结果一致的只贴一份):
# cat app.log
2026-09-22 10:00:01 INFO user=alice ip=10.0.0.1 cost=120ms
2026-09-22 10:00:02 ERROR user=bob ip=10.0.0.2 cost=3500ms
2026-09-22 10:00:03 WARN user=alice ip=10.0.0.1 cost=800ms
2026-09-22 10:00:04 error user=carol ip=10.0.0.3 cost=50ms
2026-09-22 10:00:05 INFO user=bob ip=10.0.0.2 cost=95ms
# cat s.csv
name,dept,salary
alice,dev,12000
bob,ops,9000
carol,dev,15000
dave,ops,9000
6.1 grep:搜内容
grep ERROR app.log # 包含 ERROR 的行
grep -i error app.log # 忽略大小写(ERROR 和 error 两行都出来)
grep -v INFO app.log # 不包含
grep -n alice app.log # 带行号:1:2026-09-22 10:00:01 INFO user=alice …
grep -c INFO app.log # 计数 → 2
grep -w ip app.log # 整词匹配
grep -E 'ERROR|WARN' app.log # 多个关键字(扩展正则)
grep -e ERROR -e WARN app.log # 同上
grep -A1 ERROR app.log # 带后 1 行;-B 前几行;-C 前后各几行
只取匹配的部分,再统计:
# grep -oE 'ip=[0-9.]+' app.log | sort | uniq -c
2 ip=10.0.0.1
2 ip=10.0.0.2
1 ip=10.0.0.3
递归搜目录:
# grep -r "port=8080" conf # conf/c.txt:port=8080 conf/a.conf:port=8080
# grep -rl "port" conf # 只列文件名
# grep -rn "8080" –include="*.conf" conf
conf/a.conf:1:port=8080
# grep -rL 9090 conf # 列出不包含的文件
在脚本里判断有没有:
# grep -q ERROR app.log && echo "有 ERROR"
有 ERROR
# grep -q FATAL app.log; echo "rc=$?"
rc=1 ← 没找到是 1
# grep FATAL nosuch.log; echo "rc=$?"
grep: nosuch.log: No such file or directory
rc=2 ← 出错是 2;加 -s 可以不打印这行错误
点号是正则,要按字面搜用 -F:
# echo 'ip=10x0x0x1' | grep -c 'ip=10.0.0.1'
1 ← 点号匹配了任意字符
# echo 'ip=10x0x0x1' | grep -cF 'ip=10.0.0.1'
0
\\d 这类写法要用 grep -P(三台都支持),grep -E 里写 [0-9]。
看配置文件去掉注释和空行:
# grep -Ev '^\\s*(#|$)' /etc/ssh/sshd_config (Rocky 9)
Include /etc/ssh/sshd_config.d/*.conf
AuthorizedKeysFile.ssh/authorized_keys
Subsystemsftp/usr/libexec/openssh/sftp-server
a+、a|b 在默认的 grep(基本正则)里不生效,是连载第八篇的一节。
6.2 sed:改内容
打印和删除行:
sed -n '2p' app.log # 第 2 行
sed -n '2,3p' app.log # 第 2~3 行
sed -n '/ERROR/p' app.log # 匹配的行
sed -n '$p' app.log # 最后一行
sed '1d' s.csv # 删第 1 行(去表头)
sed '/ops/d' s.csv # 删匹配的行
sed '/^$/d' 文件 # 删空行
sed 's/[[:space:]]*$//' 文件 # 删行尾空白
替换:
# sed 's/0/X/' app.log | head -1 # 每行只替换第一个
2X26-09-22 10:00:01 INFO user=alice ip=10.0.0.1 cost=120ms
# sed 's/0/X/g' app.log | head -1 # g:全部替换
2X26-X9-22 1X:XX:X1 INFO user=alice ip=1X.X.X.1 cost=12Xms
# sed 's#10.0.0.1#192.168.1.1#g' app.log # 内容里有 / 时,换一个分隔符
# sed -E 's/cost=([0-9]+)ms/\\1/' app.log # -E 扩展正则,\\1 引用分组
直接改文件,-i.bak 先留备份:
# sed -i 's/ops/OPS/g' t.csv
# sed -i.bak 's/dev/DEV/' t.csv; ls t.csv*
t.csv
t.csv.bak
插入、追加、替换整行:
# sed -i '1i # header comment' u.csv # 第 1 行前插入
# sed -i '$a eve,dev,11000' u.csv # 最后追加
# sed -i '/^bob/a after-bob' u.csv # 匹配行之后
# sed -i '/^carol/i before-carol' u.csv # 匹配行之前
# sed -i '3c REPLACED-LINE-3' u.csv # 替换第 3 行
# cat -n u.csv
1# header comment
2name,dept,salary
3REPLACED-LINE-3
4bob,ops,9000
5after-bob
6before-carol
7carol,dev,15000
8dave,ops,9000
9eve,dev,11000
改配置项、批量改多个文件:
sed -i 's/^port=.*/port=7070/' conf/a.conf conf/sub/b.conf
grep -rl 7070 conf | xargs sed -i 's/7070/6060/g'
sed -i 会把软链接替换成普通文件;macOS 的 sed -i '' 在 Linux 上是错的 —— 连载第八篇。
6.3 awk:按列处理
默认按空白分列,$1 第一列,$NF 最后一列;-F 指定分隔符:
# awk '{print $3}' app.log
INFO
ERROR
WARN
error
INFO
# awk -F, 'NR>1{print $1":"$3}' s.csv # NR>1 跳过表头
alice:12000
bob:9000
carol:15000
dave:9000
按条件过滤:
awk -F, 'NR>1 && $3>10000' s.csv # 工资大于 1 万的行
awk -F, '$2=="ops"{print $1}' s.csv # 部门是 ops 的名字 → bob dave
awk '/ERROR/' app.log # 相当于 grep
awk 'NR>=2 && NR<=3{print NR": "$3}' app.log
awk -F: '$3>=1000 && $3<65534{print $1}' /etc/passwd # 列出普通用户
求和、平均、分组统计:
# awk -F, 'NR>1{s+=$3} END{print "总和",s, "平均",s/(NR-1)}' s.csv
总和 45000 平均 11250
# awk -F, 'NR>1{a[$2]+=$3} END{for(k in a) print k, a[k]}' s.csv | sort
dev 27000
ops 18000
# awk '{c[$3]++} END{for(k in c) print c[k], k}' app.log | sort -rn
2 INFO
1 error
1 WARN
1 ERROR
分隔符写成正则,把 cost= 和 ms 之间的数字取出来:
# awk -F'cost=|ms' '{print $2}' app.log | sort -n | tail -1
3500
# awk -v t=500 -F'cost=|ms' '$2>t{print $2}' app.log # -v 传变量
3500
800
其它:awk '!seen[$4]++' 按第 4 列去重(保留第一次出现);awk 'END{print NR}' 数行数;awk -F, 'BEGIN{OFS="|"} {$1=$1; print}' 换输出分隔符。
6.4 sort / uniq
# sort -t, -k3 -nr s.csv | head -2 # 按第 3 列数字倒序
carol,dev,15000
alice,dev,12000
# sort -t, -k2,2 -k3,3nr s.csv # 先按部门,再按工资倒序
# printf '10\\n9\\n100\\n' | sort # 不加 -n 是按字符串排:10 100 9
# printf '10\\n9\\n100\\n' | sort -n # 9 10 100
# printf '1K\\n2M\\n512\\n' | sort -h # 带单位:512 1K 2M(配合 du -h 用)
🔴 uniq 只去掉相邻的重复,先 sort 再 uniq:
# printf 'b\\na\\nb\\nc\\na\\n' | uniq | wc -l
5 ← 一个没去掉
# printf 'b\\na\\nb\\nc\\na\\n' | sort | uniq -c
2 a
2 b
1 c
uniq -d 只看重复的,uniq -u 只看不重复的,sort -u 排序加去重一步完成。
统计出现次数最多的,这个组合最常用:
# awk '{print $4}' app.log | sort | uniq -c | sort -rn
2 user=bob
2 user=alice
1 user=carol
6.5 cut / tr / paste / column
cut -d, -f1,3 s.csv # 第 1、3 列
cut -c1-10 app.log # 每行前 10 个字符
cut -d: -f1,7 /etc/passwd # 用户名和 shell
⚠️ cut 遇到连续空格会切出空字段,用 awk 或先 tr -s ' ' 压缩空格:
# echo "a b" | cut -d' ' -f2 | cat -A
$ ← 空的
# echo "a b" | tr -s ' ' | cut -d' ' -f2
b
echo "Hello World" | tr a-z A-Z # HELLO WORLD
echo "a,b,c" | tr ',' '\\n' # 一行变三行
tr -d '\\r' < win.txt > unix.txt # 删掉 Windows 换行里的 \\r
paste -d, n1 n2 # 两个文件按行拼起来:1,a 2,b 3,c
paste -sd, n1 # 多行合成一行:1,2,3
column -t -s, s.csv # CSV 对齐显示
6.6 比较文件
# diff s.csv s2.csv; echo "rc=$?"
3c3
< bob,ops,9000
—
> bob,ops,9500
…
rc=1 ← 有差异是 1,相同是 0
# diff -u s.csv s2.csv # 统一格式(补丁用的格式)
@@ -1,5 +1,6 @@
name,dept,salary
alice,dev,12000
-bob,ops,9000
+bob,ops,9500
…
# diff -q s.csv s2.csv # 只说是否不同
Files s.csv and s2.csv differ
# diff -rq conf conf2 # 比较两个目录
Files conf/c.txt and conf2/c.txt differ
comm -12 a b 取两个已排序文件的交集,comm -23 a b 取只在 a 里的行。
6.7 大文件
split -l 30000 big.txt part_ # 按行数切:part_aa part_ab …
split -b 200K -d big.txt chunk_ # 按大小切,数字后缀:chunk_00 chunk_01 …
cat part_* > big2.txt; cmp big.txt big2.txt && echo 合并一致
sed -n '50000,50002p' big.txt # 只看中间几行,不用打开整个文件
tail -f app.log # 盯着新增内容;日志轮转后要用 tail -F
tail -f | grep | 下一条命令 结果迟迟不出来(缓冲),要给中间的 grep 加 –line-buffered —— 连载第八篇。
6.8 编码与 Windows 换行
GBK 和 UTF-8 互转:
# printf '中文\\n' > u8.txt; iconv -f UTF-8 -t GBK u8.txt > gbk.txt
# file u8.txt gbk.txt
u8.txt: UTF-8 Unicode text
gbk.txt: ISO-8859 text ← file 认不出 GBK,显示成 ISO-8859
# iconv -f GBK -t UTF-8 gbk.txt
中文
Windows 换行(CRLF):
# file win.sh; cat -A win.sh
win.sh: ASCII text, with CRLF line terminators
echo hi^M$
echo there^M$
# sed -i 's/\\r$//' win.sh; cat -A win.sh
echo hi$
echo there$
有 dos2unix 就直接 dos2unix 文件(我这台 CentOS 有,两台云镜像没有,包名就叫 dos2unix)。CRLF 的脚本执行时会报 bad interpreter,见第 18 章。
6.9 vim 保命操作
下面每一组按键我都用 vi -s 按键文件 目标文件 让 vim 真的执行了一遍,再看文件内容。
| 删一行、粘到最后 | dd G p :wq | 第 1 行跑到了最后 |
| 复制一行 | yy p | 当前行下面多了一份 |
| 全文替换 | :%s/line/L/g 回车 :wq | 所有 line 变成 L |
| 撤销 | u | dd 之后按 u,那行回来了 |
| 不保存退出 | :q! | 文件没变 |
| 搜索 | /L1 回车 | 光标跳到 L1,再按 x 删掉了一个字符 |
| 在最后插入一行 | G o 输入内容 Esc :wq | 最后多了一行 |
| 删第 2 行 | :2d | |
| 另存为 | :w /路径/新文件 |
⚠️ Ubuntu 的 vim 会记住上次退出时的光标位置。同样的 yyp,在 Rocky 上复制的是第 1 行,在 Ubuntu 上复制的是最后一行(上次退出时光标停在那儿)。按键之前先按 gg 回到开头,别假设光标在第 1 行。
(Rocky 云镜像只有 vi(vim-minimal),完整 vim 装 vim-enhanced;Ubuntu 装 vim。)
7. 压缩与解压
7.1 tar:最常用
tar -czf a.tar.gz src # 打包并 gzip 压缩
tar -cjf a.tar.bz2 src # bzip2(需要 bzip2 包)
tar -cJf a.tar.xz src # xz,压得最小,也最慢
tar -cf a.tar src # 只打包不压缩
tar -tf a.tar.gz # 看里面有什么;-tvf 带权限和大小
tar -xzf a.tar.gz -C /目标目录 # 解压到指定目录
tar -xf a.tar.xz -C x2 # 解压时不写 z/j/J 也行,tar 会自己识别
同一份 1.3M 的测试数据(数字序列,很好压):
-rw-r–r–. 1 root root 1300480 Sep 22 20:03 a.tar
-rw-r–r–. 1 root root 258119 Sep 22 20:03 a.tar.bz2
-rw-r–r–. 1 root root 428955 Sep 22 20:03 a.tar.gz
-rw-r–r–. 1 root root 41264 Sep 22 20:03 a.tar.xz
只解压一个文件、去掉顶层目录、按通配符解压:
tar -xzf a.tar.gz -C x4 src/sub/app.conf # 路径要写成 tar -tf 里显示的样子
tar -xzf a.tar.gz -C x5 –strip-components=1 # 去掉第一层 src/
tar -xzf a.tar.gz -C x6 –wildcards '*.log'
排除:
# tar -czf ex.tar.gz –exclude='src/logs' –exclude='*.log' src; tar -tzf ex.tar.gz
src/
src/sub/
src/sub/app.conf
src/data.txt
src/link
绝对路径:tar 会自动去掉开头的 /;想要干净的相对路径,用 -C 先切目录:
# tar -czf abs.tar.gz /tmp/lab7/src/sub
tar: Removing leading `/' from member names
# tar -czf rel.tar.gz -C /tmp/lab7 src/sub; tar -tzf rel.tar.gz
src/sub/
src/sub/app.conf
常见报错:
# tar -xzf a.tar.gz -C /nosuch
tar: /nosuch: Cannot open: No such file or directory ← -C 的目录要先建好
# tar -xzf a.tar.xz -C x1
gzip: stdin: not in gzip format ← 格式写错了(xz 当成 gz 解)
# tar -rzf a.tar.gz src/data.txt
tar: Cannot update compressed archives ← 压缩包不能追加,只有 .tar 能 -r
tar -cfz 会生成一个叫 z 的文件 —— 选项顺序的坑,连载第八篇。
权限与属主:tar -xpf 保留权限(root 解压时默认就保留);打包时 –owner=0 –group=0 把属主统一成 root。
zstd:Rocky 9(tar 1.34)、Ubuntu 24.04 支持 tar –zstd -cf a.tar.zst src(要装 zstd);CentOS 7 的 tar 1.26 不认这个选项:tar: unrecognized option '–zstd'。
多线程 xz:tar -cf – src | xz -T0 > a.tar.xz。
7.2 gzip / xz / bzip2:压单个文件
gzip d1 # 压完 d1 就没了,只剩 d1.gz
gunzip d1.gz # 解压,d1.gz 没了
gzip -k d1 # 保留原文件(CentOS 7 的 gzip 没有 -k:gzip: invalid option — 'k')
gzip -c d1 > d1.gz # 三个系统都能用的「保留原文件」写法
zcat d2.gz | tail -1 # 不解压直接看
zgrep -c 99999 d2.gz # 不解压直接搜
gzip -l d2.gz # 看压缩率
gzip -t d2.gz # 检查完整性
xz d3; unxz d3.xz; xz -k d3; xzcat d3.xz
bzip2 -k d3
7.3 zip / unzip
zip -r z.zip src # 压目录要 -r
unzip -l z.zip # 看内容
unzip -q z.zip -d z1 # 解压到指定目录
unzip -o -q z.zip -d z1 # 覆盖已有文件,不问
zip -q -r -P secret zp.zip src/sub # 加密码
unzip -P secret zp.zip -d zp1
zip -q -r zx.zip src -x 'src/logs/*' # 排除
zip -q -j zj.zip src/sub/app.conf # 不要目录结构,只放文件
unzip -j z.zip 'src/sub/*' -d zonly # 只解压某个目录下的文件,并去掉目录
unzip -p z.zip src/sub/app.conf # 解压到标准输出
密码错误时:skipping: src/sub/app.conf incorrect password。
目标文件已存在又没加 -o 时,unzip 会问 replace …? [y]es, [n]o, [A]ll, [N]one, [r]ename:,脚本里读不到回答就按 None 处理(一个都不覆盖)。
7.4 Windows 压缩的 zip,中文文件名乱码
Windows 自带的压缩把中文文件名按 GBK 存。我用一个这样的 zip 在三台上测:
# unzip -l gbk.zip | sed -n '4,5p'
5 01-01-1980 00:00 ╓╨╬──┐┬╝/▒и╕ц.txt ← 乱码
# unzip -O GBK -l gbk.zip | sed -n '4,5p'
5 01-01-1980 00:00 中文目录/报告.txt
3 01-01-1980 00:00 中文目录/数据.csv
# unzip -q -O GBK gbk.zip -d d2; find d2 -type f
d2/中文目录/报告.txt
d2/中文目录/数据.csv
✅ unzip -O GBK,三台都有效,包括 CentOS 7 的 unzip 6.0-24.el7_9。不加 -O 解出来的文件名是乱码;用 7za x 解出来也是乱码。
7.5 分卷、校验
大文件切开传,再合起来:
tar -czf – src | split -b 300K – big.tar.gz.part_ # 边压边切
cat big.tar.gz.part_* | tar -tzf – | head -2 # 不合并直接检查
cat big.tar.gz.part_* > joined.tar.gz
校验:
# md5sum a.tar.gz > a.md5; md5sum -c a.md5
a.tar.gz: OK
# echo x >> a.tar.gz; md5sum -c a.md5; echo "rc=$?"
a.tar.gz: FAILED
md5sum: WARNING: 1 computed checksum did NOT match
rc=1
# gzip -t a.tar.gz
gzip: a.tar.gz: decompression OK, trailing garbage ignored ← gzip 只是警告
# tar -tzf a.tar.gz > /dev/null; echo "tar rc=$?"
tar rc=2 ← tar 判为出错
不落地打包:tar -czf – src | ssh 远程机器 'tar -xzf – -C /tmp/'(第 8 章)。
8. 上传下载与文件传输
本章大多在 Rocky(发)和 Ubuntu(收)之间实测,两台配好了密钥登录;CentOS 7 的 OpenSSH 是 7.4、rsync 是 3.1.2,另外单独测过 Windows 往 CentOS 传。
8.1 选哪个工具
| 传一两个文件 | scp |
| 传目录、要增量、要断点续传、要同步删除 | rsync |
| 从网址下载 | curl -O / wget |
| 临时让别人从我这下载 | python3 -m http.server |
| 两台机器之间没有 ssh,只有一个端口 | nc |
| 大量小文件 | tar | ssh 管道 |
8.2 scp
scp 文件 root@10.115.180.200:/tmp/ # 传过去
scp root@10.115.180.200:/etc/hostname /tmp/ # 拉回来
scp -r 目录 root@10.115.180.200:/tmp/ # 传目录必须 -r
scp -P 2222 文件 root@IP:/tmp/ # 指定端口,大写 P
scp -p 文件 root@IP:/tmp/ # 保留时间
scp root@IP:'/etc/host*' /tmp/ # 远程通配符要加引号
默认不保留修改时间,-p 才保留:
-rw-r—– 1 root root 2 Sep 22 12:07 /tmp/a.conf ← scp
-rw-r—– 1 root root 2 Jan 1 2020 /tmp/a.p.conf ← scp -p
常见报错:
# scp /data/src root@10.115.180.200:/tmp/ (传目录忘了 -r,Rocky 9 的 OpenSSH 9.9)
scp: local "/data/src" is not a regular file
scp: failed to upload file /data/src to /tmp/
# scp -p 22 /data/src/a.conf root@10.115.180.200:/tmp/ (端口写成了小写 -p)
scp: stat local "22": No such file or directory
新版 scp 底层走的是 sftp 协议(scp -v 里能看到 command sftp),遇到只支持老协议的服务器加 -O。连载第八篇有细节。
8.3 rsync
两端都要装 rsync。我这台 Rocky 云镜像一开始没有:
# rsync -av /data/src root@10.115.180.200:/tmp/r1/ (本机没装)
bash: line 1: rsync: command not found
# rsync -av /data/src/a.conf root@10.115.180.200:/tmp/r9/ (本机装了,对方没装)
bash: line 1: rsync: command not found
rsync: connection unexpectedly closed (0 bytes received so far) [sender]
rsync error: error in rsync protocol data stream (code 12) at io.c(285) [sender=3.2.7]
🔴 源目录末尾有没有斜杠,结果完全不同:
rsync -av /data/src root@IP:/tmp/r1/ # → /tmp/r1/src/big.txt(整个目录传过去)
rsync -av /data/src/ root@IP:/tmp/r2/ # → /tmp/r2/big.txt(只传目录里的内容)
第二次只传有变化的:
# rsync -av /data/src/ root@IP:/tmp/r2/ | tail -2 (第一次)
sent 589,319 bytes received 114 bytes 1,178,866.00 bytes/sec
# rsync -av /data/src/ root@IP:/tmp/r2/ | tail -2 (没改动,再来一次)
sent 149 bytes received 13 bytes 324.00 bytes/sec
同步删除:目标里多出来的文件,加 –delete 才会删。先用 -n 预演:
# rsync -avn –delete /data/src/ root@IP:/tmp/r2/
deleting extra.txt
total size is 588,901 speedup is 3,067.19 (DRY RUN)
🔴 –delete 会删掉目标里「源没有」的所有文件,源目录写错时删的就是一大片。每次先 -n 看一眼它要删什么。
其它常用参数(都测过):
rsync -av –exclude='*.txt' –exclude='sub/' src/ root@IP:/tmp/r3/ # 排除
rsync -avz –progress 大文件 root@IP:/tmp/ # 压缩传输,显示进度
rsync -aP 大文件 root@IP:/tmp/ # -P 等于 –partial –progress
rsync -av -e 'ssh -p 2222' 文件 root@IP:/tmp/ # 指定 ssh 端口
rsync -av –bwlimit=100 大文件 root@IP:/tmp/ # 限速 100KB/s
rsync -av root@IP:/etc/hostname /tmp/pull/ # 拉取
rsync -a /data/src /tmp/localcopy/ # 本机两个目录之间同步
-a 会保留时间和权限;只写 -r 不保留时间(目标文件的时间变成了传输时的时间)。
8.4 sftp 批处理
# cat /tmp/sftp.batch
cd /tmp
put /data/src/a.conf sftp_a.conf
ls -l sftp_a.conf
get /etc/hostname /tmp/sftp_hostname
bye
# sftp -b /tmp/sftp.batch root@10.115.180.200
sftp> cd /tmp
sftp> put /data/src/a.conf sftp_a.conf
sftp> ls -l sftp_a.conf
-rw-r—– ? root root 4 Sep 22 12:08 sftp_a.conf
sftp> get /etc/hostname /tmp/sftp_hostname
sftp> bye
8.5 curl 与 wget 下载
curl -O http://10.115.180.19:8000/a.conf # 保存成原文件名
curl -o renamed.conf http://…/a.conf # 指定文件名
curl -I http://…/big.txt # 只看响应头
curl -L http://github.com # 跟随跳转(不加 -L 拿到的是 301)
curl -o /dev/null -w '%{http_code}\\n' URL # 只要状态码
wget http://…/a.conf -O w.conf
wget -q -r -np -nH -P mirror http://…/sub/ # 递归下载一个目录
🔴 curl -O 下载失败时,退出码是 0,还会把错误页存成文件:
# curl -sS -O http://10.115.180.19:8000/nosuch; echo "rc=$?"; ls -l nosuch
rc=0
-rw-r–r– 1 root root 469 Sep 22 12:08 nosuch
# head -c 80 nosuch
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
✅ 脚本里下载一律加 -f,失败时返回 22 且不落文件:
# curl -fsS -O http://10.115.180.19:8000/nosuch2; echo "rc=$?"
curl: (22) The requested URL returned error: 404
rc=22
断点续传(服务器要支持 Range;我用阿里云镜像上一个 38MB 的文件测):
# curl -sS -r 0-999999 -o part.gz URL; ls -l part.gz # 先只下 1MB,模拟中断
-rw-r–r– 1 root root 1000000 Sep 22 12:12 part.gz
# curl -sS -C – -o part.gz URL; ls -l part.gz # -C – 接着下
-rw-r–r– 1 root root 38533055 Sep 22 12:12 part.gz
# gzip -t part.gz && echo 续传后完整
续传后完整
wget -c 同样能续传,续完和完整下载的文件 cmp 一致;文件已经完整时 wget -c 不会重下。
⚠️ Python 自带的 http.server 不支持 Range,用它测续传会以为续传不生效。
其它:
curl –limit-rate 200k -o /dev/null URL # 限速
curl -k https://自签名证书的网站 # 不加 -k:curl: (60) SSL certificate problem: self-signed certificate
curl -X POST -H 'Content-Type: application/json' -d '{"a":1}' URL
curl -u user:pass URL # HTTP 基本认证
curl -w 'dns=%{time_namelookup} connect=%{time_connect} total=%{time_total}\\n' -o /dev/null -s URL # 分阶段耗时,第 9 章
8.6 临时起一个下载服务
cd /data/src && python3 -m http.server 8000 # CentOS 7 上是 python -m SimpleHTTPServer 8000
firewall-cmd –add-port=8000/tcp # RHEL 系记得开端口(临时的,reload 就没了)
别人用 curl -O http://你的IP:8000/文件名 或浏览器下载。用完 Ctrl+C。
8.7 nc:没有 ssh 时传文件
# 接收端(Rocky,要装 nmap-ncat,并开放 9999 端口)
nc -l 9999 > /tmp/nc_recv.txt
# 发送端(Ubuntu,netcat-openbsd)
nc -N 10.115.180.19 9999 < /etc/os-release
-N:发完关掉连接,不加的话发送端不退出。
8.8 tar + ssh:大量小文件
tar -czf – -C /data src | ssh root@10.115.180.200 'tar -xzf – -C /tmp/' # 推过去
ssh root@10.115.180.200 'tar -czf – -C /etc ssh' | tar -tzf – | head -2 # 拉回来(这里只列目录)
两端都要有 tar。我这台 Rocky 云镜像一开始没有,报 bash: line 1: tar: command not found。
远程执行一条命令,结果存到本地:ssh root@IP 'cat /etc/hostname' > /tmp/remote_out.txt。
8.9 传完校验
# md5sum /data/src/big.txt; ssh root@10.115.180.200 'md5sum /tmp/r4/big.txt'
dea9193b768319cbb4ff1a137ac03113 /data/src/big.txt
dea9193b768319cbb4ff1a137ac03113 /tmp/r4/big.txt
sha256sum 同理。
8.10 从 Windows 传到 Linux
我这台 Windows 11 自带 OpenSSH 客户端(C:\\Windows\\System32\\OpenSSH\\scp.exe),PowerShell 里直接用:
scp -i C:\\Users\\你\\.ssh\\id_ed25519 D:\\file.txt root@192.168.3.101:/tmp/
scp root@192.168.3.101:/etc/hostname .\\
🔴 私钥文件权限太宽,Windows 版 ssh/scp 会拒绝使用。我给私钥加上 Users 组的读权限后:
Bad permissions. Try removing permissions for user: BUILTIN\\\\Users (S-1-5-32-545) on file C:/Users/Mikko/labkey_win_test.
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions for 'C:/Users/Mikko/labkey_win_test' are too open.
退出码 255。收紧权限之后就正常了,我在 PowerShell 里执行的是:
icacls 私钥文件 /inheritance:r /grant:r "${env:USERNAME}:(R)" /remove "BUILTIN\\Users" "BUILTIN\\Administrators" "NT AUTHORITY\\SYSTEM"
/inheritance:r 去掉从上级目录继承来的权限,/grant:r 只给自己读,/remove 删掉直接加在文件上的其它用户(报错里点名了哪个用户,就一定要把它删掉)。执行后 icacls 私钥文件 只剩一行 DESKTOP-MIKKO\\Mikko:(R),再 scp 就成功了。
另外,Windows PowerShell 5.1 会把 ssh 打到 stderr 的普通提示(比如 Warning: Permanently added …)显示成红色的 NativeCommandError,看退出码,不看颜色,那次退出码是 0。
9. 网络:看配置、测连通、查端口、DNS、抓包、改 IP
9.1 看 IP、路由、网卡
# ip -br addr (CentOS,简洁格式)
lo UNKNOWN 127.0.0.1/8 ::1/128
ens33 UP 192.168.3.101/24 2409:8a50:…:d70/64 fe80::20c:29ff:feb0:d70/64
# ip -4 -br addr show scope global # 只看 IPv4 的真实地址
ens33 UP 192.168.3.101/24
# ip route
default via 192.168.3.1 dev ens33
192.168.3.0/24 dev ens33 proto kernel scope link src 192.168.3.101
# ip route get 223.5.5.5 # 去某个地址走哪个网卡、哪个网关
223.5.5.5 via 192.168.3.1 dev ens33 src 192.168.3.101
# ip -s link show ens33 # 收发包数、错误、丢包
RX: bytes packets errors dropped overrun mcast
219245049 224287 0 0 0 0
# ip neigh # ARP 表
192.168.3.1 dev ens33 lladdr 88:81:b9:2b:7e:99 REACHABLE
ifconfig、netstat、route、arp 属于 net-tools,我这台 CentOS 和 Rocky 都没装(command not found),要用就装 net-tools。新写的脚本用 ip 和 ss。
9.2 DNS 配置在哪看
| CentOS 7(我这台由 dhclient 管) | /etc/resolv.conf:; generated by /usr/sbin/dhclient-script / nameserver 192.168.3.1 |
| Rocky 9(NetworkManager) | /etc/resolv.conf,或 nmcli dev show 网卡 | grep DNS |
| Ubuntu 24.04 | /etc/resolv.conf 是软链,里面只有 nameserver 127.0.0.53;真正的上游要看 resolvectl status |
# ls -l /etc/resolv.conf (Ubuntu)
lrwxrwxrwx 1 root root 39 Sep 20 07:46 /etc/resolv.conf -> ../run/systemd/resolve/stub-resolv.conf
# resolvectl status | grep -E 'Current DNS|DNS Servers'
Current DNS Server: 10.115.180.1
DNS Servers: 10.115.180.1 fd42:7e9b:f1d3:c525::1 fe80::216:3eff:feca:e4d9
手改 /etc/resolv.conf 过一会儿就被覆盖,是连载第五篇的一节;改 DNS 要改网络配置(9.8)。
9.3 查域名
# dig www.baidu.com +short
www.a.shifen.com.
111.45.11.5
183.240.99.224
# dig @223.5.5.5 www.baidu.com A +short # 指定 DNS 服务器
# dig -x 223.5.5.5 +short # 反查
public1.alidns.com.
# dig nosuch.invalid | grep status
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 44905
# dig www.baidu.com | grep -E 'Query time|SERVER'
;; Query time: 2 msec
;; SERVER: 192.168.3.1#53(192.168.3.1)
# nslookup -type=mx qq.com | grep 'mail exchanger'
qq.commail exchanger = 30 mx1.qq.com.
# getent hosts www.baidu.com # 按系统的解析顺序查(会先看 /etc/hosts)
dig/nslookup/host 在 bind-utils(Ubuntu 是 bind9-dnsutils)里。dig 不看 /etc/hosts,getent、ping 看 —— 所以改了 hosts 以后 dig 查不到新值,是连载第五篇的一节。
9.4 测连通
# ping -c 3 -W 2 223.5.5.5 | tail -2
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 28.009/28.258/28.442/0.266 ms
# ping -c 1 -W 1 192.0.2.1; echo "rc=$?"
1 packets transmitted, 0 received, 100% packet loss, time 0ms
rc=1 ← 不通是 1
# ping -c 2 -W 1 nosuch.invalid; echo "rc=$?"
ping: nosuch.invalid: Name or service not known
rc=2 ← 域名解析失败是 2
看路径:
# traceroute -n -m 6 -w 1 223.5.5.5 (Rocky,第一跳是宿主机网桥)
1 10.115.180.1 0.280 ms 0.258 ms 0.252 ms
2 192.168.3.1 0.641 ms 0.637 ms 0.795 ms
3 10.17.0.1 8.314 ms 8.308 ms 8.304 ms
# mtr -rnc 3 -m 6 223.5.5.5 # 每一跳的丢包率,排查「时通时不通」
HOST: rocky9 Loss% Snt Last Avg Best Wrst StDev
1.|– 10.115.180.1 0.0% 3 0.6 0.6 0.6 0.7 0.1
2.|– 192.168.3.1 0.0% 3 1.4 1.3 1.2 1.4 0.1
tracepath 不用 root,还会顺便报出路径 MTU(我这边是 pmtu 1492,所以 ping -s 1472 -M do 发不过去;MTU 排查见连载第五篇)。
9.5 测端口通不通
# timeout 3 bash -c 'echo > /dev/tcp/127.0.0.1/22' && echo "22 通"
22 通
# timeout 3 bash -c 'echo > /dev/tcp/127.0.0.1/23'; echo "rc=$?"
bash: connect: Connection refused
rc=1
# nc -zv -w 2 127.0.0.1 22
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Connected to 127.0.0.1:22.
# echo quit | timeout 5 telnet 127.0.0.1 22
Trying 127.0.0.1…
Connected to 127.0.0.1.
# nmap -Pn -p 22,80,443 127.0.0.1 | grep tcp
22/tcp open ssh
80/tcp closed http
443/tcp closed https
/dev/tcp 是 bash 的功能,不用装任何东西;在 sh 里不能用(连载第五篇)。
9.6 看谁在监听、谁连着我
# ss -lntp (Rocky)
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=751,fd=7))
LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=751,fd=8))
参数:-l 监听、-n 不解析名字、-t TCP、-u UDP、-p 显示进程。
ss -lnup # UDP 监听
ss -tnp state established # 已建立的连接
ss -s # 汇总:Total、estab、timewait 各多少
ss -tnp '( sport = :22 )' # 本机 22 端口上的连接
lsof -i :22 -P -n # 谁占着 22 端口
fuser 22/tcp # 同上,只列 PID(psmisc 包)
⚠️ Ubuntu 24.04 上 22 端口会显示 systemd 和 sshd 都在监听:SSH 是由 ssh.socket 按需拉起的(改端口的坑见第 15 章、连载第五篇)。
Local Address 是 127.0.0.1:端口 的服务,只有本机能访问,外面连它是 Connection refused,防火墙开了也没用。
9.7 抓包
# timeout 8 tcpdump -i any -nn -c 4 icmp (另开一个窗口 ping 223.5.5.5)
20:17:12.645269 IP 192.168.3.101 > 223.5.5.5: ICMP echo request, id 17306, seq 1, length 64
20:17:12.673921 IP 223.5.5.5 > 192.168.3.101: ICMP echo reply, id 17306, seq 1, length 64
…
4 packets captured
tcpdump -i any -nn -c 3 -w /tmp/cap.pcap 'tcp port 80' # 存文件,可以拿到 Wireshark 里看
tcpdump -nn -r /tmp/cap.pcap # 读文件
9.8 请求慢,慢在哪一步
# curl -sS -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} first_byte=%{time_starttransfer} total=%{time_total} code=%{http_code}\\n' https://www.baidu.com
dns=0.013 connect=0.039 tls=0.152 first_byte=0.180 total=0.181 code=200
各个值是从请求开始累计的时间。dns 大就是 DNS 慢;first_byte 减 tls 大,就是服务端处理慢。
查本机的公网出口 IP:curl -sS ifconfig.me。
9.9 改主机名
# hostnamectl set-hostname lab-rocky9-new
# hostname; cat /etc/hostname
lab-rocky9-new
lab-rocky9-new
立即生效,不用重启。建议同时在 /etc/hosts 加一行 127.0.1.1 新主机名。
9.10 改 IP、加路由、改 DNS
⚠️ 远程改网络配置,先想好改错了怎么回来(控制台、带外管理)。下面加的都是「附加地址」,不影响原来的地址,所以远程测试时 SSH 没断。
临时加一个 IP(三台通用,重启就没):
ip addr add 10.99.99.9/24 dev enp5s0
ip addr del 10.99.99.9/24 dev enp5s0
Rocky 9:nmcli
# nmcli -f NAME,DEVICE,FILENAME con show # 连接名,以及配置存在哪个文件
NAME DEVICE FILENAME
System enp5s0 enp5s0 /etc/sysconfig/network-scripts/ifcfg-enp5s0
# nmcli con mod "System enp5s0" +ipv4.addresses 10.99.99.9/24
# ip -br addr | grep 10.99 || echo "(mod 之后还没生效)"
(mod 之后还没生效)
# nmcli con up "System enp5s0" # up 一次才生效
# ip -br addr | grep 10.99
enp5s0 UP 10.99.99.9/24 10.115.180.19/24 fe80::216:3eff:fe8f:fb46/64
改成静态 IP(我用的是它现在的地址,所以 SSH 没断;换成别的地址,当前的 SSH 连接会断):
# nmcli con mod "System enp5s0" ipv4.method manual ipv4.addresses 10.115.180.19/24 ipv4.gateway 10.115.180.1 ipv4.dns 10.115.180.1
# nmcli con up "System enp5s0"
Connection successfully activated (D-Bus active path: /org/freedesktop/NetworkManager/ActiveConnection/3)
# ip route | head -1
default via 10.115.180.1 dev enp5s0 proto static metric 100 ← 原来是 proto dhcp
加路由、改 DNS:
# nmcli con mod "System enp5s0" +ipv4.routes "10.88.0.0/16 10.115.180.1"
# nmcli con mod "System enp5s0" ipv4.dns "223.5.5.5 119.29.29.29"
# nmcli con up "System enp5s0"
# ip route | grep 10.88; grep nameserver /etc/resolv.conf
10.88.0.0/16 via 10.115.180.1 dev enp5s0 proto static metric 100
nameserver 223.5.5.5
nameserver 119.29.29.29
nameserver 10.115.180.1
删除用减号:nmcli con mod "System enp5s0" -ipv4.addresses 10.99.99.9/24。
我这台 Rocky 的配置是 cloud-init 生成的 ifcfg 文件,所以在 network-scripts 目录下;不管配置文件在哪,nmcli 的用法都一样。
Ubuntu 24.04:netplan
# cat /etc/netplan/50-cloud-init.yaml
network:
version: 2
ethernets:
enp5s0:
match:
macaddress: "00:16:3e:47:a0:20"
dhcp4: true
dhcp6: true
set-name: "enp5s0"
新建一个文件,给网卡加一个静态地址、一条路由、一个 DNS:
# /etc/netplan/60-lab.yaml
network:
version: 2
ethernets:
enp5s0:
addresses: [10.99.99.8/24]
routes:
– to: 10.88.0.0/16
via: 10.115.180.1
nameservers:
addresses: [223.5.5.5]
chmod 600 /etc/netplan/60-lab.yaml # 权限太宽会反复警告:Permissions for … are too open
netplan generate # 先检查语法
netplan apply
多个文件会合并:netplan get ethernets.enp5s0 能看到合并后的结果,DHCP 保留、静态地址加上了。
🔴 netplan apply 那一刻 DHCP 地址和默认路由会短暂消失。我在 apply 后立即看、8 秒后再看:
— apply 后立即:
enp5s0 UP 10.99.99.8/24 fe80::216:3eff:fe47:a020/64
— 8 秒后:
enp5s0 UP 10.99.99.8/24 10.115.180.200/24 metric 100 …
default via 10.115.180.1 dev enp5s0 proto dhcp src 10.115.180.200 metric 100
10.88.0.0/16 via 10.115.180.1 dev enp5s0 proto static
远程执行 netplan apply 会断几秒;如果改错了 IP,就回不来了。没把握时用 netplan try:它应用新配置后等你按回车确认,超时不确认就回滚:
# netplan try –timeout 10 (不按回车)
Changes will revert in 10 seconds…
Reverting.
# ip -br addr show enp5s0
enp5s0 UP 10.115.180.200/24 metric 100 … ← 10.99.99.8 没加上,回滚了
# ls /etc/netplan/
50-cloud-init.yaml
60-lab.yaml ← 🔴 但文件还在!
🔴 netplan try 回滚的只是当前生效的配置,你写的 yaml 文件不会被删。下次 netplan apply 或重启,它照样生效。试失败了,记得把文件改回来或删掉。
YAML 缩进错了:netplan generate 报 Invalid YAML: inconsistent indentation,并指出行号。
生成的最终配置在 /run/systemd/network/10-netplan-enp5s0.network。
CentOS 7:我这台的 NetworkManager 是停用的,IP 由 network 脚本 + dhclient 管,配置在 /etc/sysconfig/network-scripts/ifcfg-ens33(BOOTPROTO="dhcp"、ONBOOT="yes" 这些键)。nmcli 在这台上会报 Error: NetworkManager is not running.。
9.11 带宽测试:iperf3
iperf3 -s -1 # 服务端(-1:测完一次就退出;RHEL 系记得开 5201 端口)
iperf3 -c 10.115.180.19 -t 3 # 客户端
iperf3 -c 10.115.180.19 -t 2 -R # 反方向
两台 LXD 虚拟机在同一台宿主机上,测出来约 100 Gbits/sec,真实网络会低得多。用了 -1 的服务端测完一次就退出,再连会报 unable to connect to server … Connection refused。
9.12 代理
export http_proxy=http://代理:端口 https_proxy=http://代理:端口
curl –noproxy '*' URL # 这一次不走代理
unset http_proxy https_proxy
代理设错了,curl 报的是连代理失败:curl: (7) Failed to connect to 127.0.0.1 port 9: Connection refused。sudo 之后代理变量没了,是连载第五篇的一节。
10. 用户与权限
10.1 看当前身份、谁在线
whoami; id; groups
who # 当前登录的用户
w # 同上,多了在干什么
last -n 3 # 最近登录记录
lastlog -u root # 某用户最后一次登录
10.2 用户信息存在哪
# head -2 /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
# ls -l /etc/passwd /etc/shadow
-rw-r–r–. 1 root root 1094 Sep 22 19:58 /etc/passwd
———-. 1 root root 697 Sep 22 19:58 /etc/shadow ← 密码哈希在这里,只有 root 能读
# awk -F: '$3>=1000 && $3<60000 {print $1, $3, $7}' /etc/passwd # 列出普通用户
# getent passwd root # 查一个用户(也查 LDAP 等)
默认值在 /etc/login.defs:两边的 UID_MIN 都是 1000;我这台 CentOS 的 UMASK 是 077,Ubuntu 是 022,而且 Ubuntu 有 HOME_MODE 0750(新用户家目录别人进不去)。
10.3 创建用户
useradd -m -s /bin/bash -c "Lab User 2" -G wheel labx2 # Ubuntu 上管理员组是 sudo,不是 wheel
id labx2
uid=1001(labx2) gid=1001(labx2) groups=1001(labx2),10(wheel)
useradd -u 3001 -d /srv/labx3home -m -s /sbin/nologin labx3 # 指定 UID、家目录、禁止登录
useradd -r -s /sbin/nologin -M labsvc # 系统用户(给服务跑程序用),不建家目录
用户已存在:useradd: user 'labx1' already exists,退出码 9。
⚠️ useradd 不加 -m,Ubuntu 上不建家目录、shell 是 /bin/sh(CentOS 因为 CREATE_HOME yes 会建)—— 连载第七篇。三个系统都写全 -m -s /bin/bash。
Ubuntu 另有交互式的 adduser,非交互写法:
# adduser –disabled-password –gecos "" labx5
info: Adding new user `labx5' to supplemental / extra groups `users' …
10.4 设密码
echo 'labx2:Lab@12345' | chpasswd # 三个系统通用,脚本里用这个
echo 'Lab@12345' | passwd –stdin labx1 # 只有 RHEL 系有 –stdin,Ubuntu 没有
passwd -S labx2 # 看状态
labx2 PS 2026-09-22 0 99999 7 -1 (Password set, SHA512 crypt.)
PS 已设密码,LK 已锁定,NP 空密码(passwd -d 删除密码后就是 NP)。
10.5 切换身份
su – labx2 # 带 -,加载对方的环境,进对方家目录
su labx2 -c 'pwd' # 不带 -,还在原来的目录(/root)
sudo -u labx2 whoami # 以别人的身份执行一条命令
sudo -u labx2 -i pwd # 相当于 su –
禁止登录的用户(shell 是 nologin):
# su – labx3 -c 'whoami'; echo "rc=$?"
This account is currently not available.
rc=1
# su -s /bin/bash labx3 -c 'whoami' ← root 可以临时指定 shell
labx3
🔴 我这台 CentOS 上 sudo 带参数就报错:
# sudo -u labx2 whoami
/var/tmp/scl3N6LIw: line 8: -u: command not found
# type -a sudo
sudo is /opt/rh/devtoolset-7/root/usr/bin/sudo
sudo is /usr/bin/sudo
原因:root 的 .bashrc 里有 source /opt/rh/devtoolset-7/enable,devtoolset 自带一个 sudo 包装脚本排在 PATH 最前面,把系统的 sudo 盖住了,而这个脚本不处理参数(它自己的注释写着 # TODO: parse & pass-through sudo options)。用全路径 /usr/bin/sudo -u labx2 whoami 就正常。装了 devtoolset / SCL 的 CentOS 7 都可能遇到。
10.6 修改用户
usermod -s /bin/sh labx2 # 改 shell
usermod -c "备注" labx2 # 改备注
usermod -l labx2new labx2 # 改用户名(家目录不会跟着改)
usermod -d /home/labx2new -m labx2new # 搬家目录
usermod -L labx2 # 锁定:shadow 里密码前面加了 !,passwd -S 显示 LK
usermod -U labx2 # 解锁
10.7 组
groupadd labg1; groupadd -g 4001 labg2
usermod -aG labg2 labx2 # 追加到组,🔴 一定要 -a
gpasswd -a labx1 labg1 # 同上的另一种写法
gpasswd -d labx1 labg1 # 移出组
groupmems -g labg1 -l # 列出组成员
groupdel labg1
🔴 usermod -G 不加 -a 是覆盖,原来的附加组全没了:
# usermod -G labg1 labx2; id labx2
uid=1001(labx2) gid=1001(labx2) groups=1001(labx2),3002(labg1) ← wheel 没了
# usermod -aG labg2 labx2; id labx2
uid=1001(labx2) gid=1001(labx2) groups=1001(labx2),3002(labg1),4001(labg2)
加了组以后,已经登录的会话不会生效,要重新登录(连载第七篇)。
10.8 密码过期、账号过期
# chage -l labx2
Last password change: Sep 22, 2026
Password expires: never
…
chage -M 90 -W 7 labx2 # 90 天必须改,提前 7 天警告
chage -E 2027-01-01 labx2 # 账号到期日
chage -E -1 labx2 # 取消到期
chage -d 0 labx2 # 下次登录必须改密码
🔴 chage -d 0 之后,Ubuntu 上连 root 执行 su – 用户 都会要求先改密码,脚本里就卡住了;CentOS 和 Rocky 上 root 可以直接 su 进去:
# su – labx2 -c 'echo 进来了' (Ubuntu 24.04)
You are required to change your password immediately (administrator enforced).
Current password: su: Authentication token manipulation error
Changing password for labx2.
Ubuntu 上 sudo -u labx2 whoami 不受影响。
10.9 sudo 授权
管理员组:RHEL 系是 wheel,Ubuntu 是 sudo(/etc/sudoers 里的 %wheel ALL=(ALL) ALL / %sudo …)。把用户加进这个组就有完整 sudo 权限。
只授权某几条命令、免密码,写到 /etc/sudoers.d/,写完一定用 visudo -c 检查:
# cat /etc/sudoers.d/labx1
labx1 ALL=(ALL) NOPASSWD: /usr/bin/systemctl status *, /usr/bin/cat /etc/shadow
# chmod 440 /etc/sudoers.d/labx1
# visudo -cf /etc/sudoers.d/labx1
/etc/sudoers.d/labx1: parsed OK
# sudo -l -U labx1 # 看某用户能 sudo 什么
labx1 登录后执行 sudo -n cat /etc/shadow 可以,执行没授权的 sudo -n ls /root 就要密码(我这台 CentOS 上普通用户是中文环境,提示是 sudo: 需要密码)。
写错一个字:
# echo 'labx1 ALL=(ALL) NOPASSWD ALL' > /tmp/bad.sudo; visudo -cf /tmp/bad.sudo; echo "rc=$?"
>>> /tmp/bad.sudo: syntax error near line 1 <<<
parse error in /tmp/bad.sudo near line 1
rc=1
🔴 sudoers.d 里有一个语法错误的文件,整个 sudo 都会挂;文件名带点号会被整个忽略 —— 这两个都在连载第七篇。先写到别处、visudo -cf 通过了再挪进去。
10.10 登录失败锁定
我这台 CentOS 7 上 faillock 和 pam_tally2 两个命令都有,Ubuntu 24.04 只有 faillock(配置文件 /etc/security/faillock.conf):
# pam_tally2 -u labx1 (CentOS 7)
Login Failures Latest failure From
labx1 0
# faillock –user labx1 (Ubuntu)
labx1:
When Type Source Valid
清零计数(解锁):faillock –user 用户 –reset(CentOS 7 和 Ubuntu 上实测),CentOS 7 还可以用 pam_tally2 -u 用户 -r。锁定次数、锁多久这些策略在 PAM 配置里,本文没有改动它们。
10.11 删除用户
userdel -r 用户 # 连家目录和邮件一起删
不带 -r,家目录和该用户的文件都留下来,属主变成一个数字:
# userdel labx1; ls -l /tmp/labx1file; ls -ld /home/labx1
-rw-r–r–. 1 1000 root 0 Sep 22 20:21 /tmp/labx1file
drwx——. 2 1000 1000 62 Sep 22 20:21 /home/labx1
# find / -xdev -nouser ← 找出这些无主文件
以后新建的用户如果拿到了同一个 UID,就会「继承」这些文件(连载第七篇)。
用户还有进程在跑,删不掉:
# userdel -r labx2; echo "rc=$?"
userdel: user labx2 is currently used by process 89019
rc=8
# pkill -KILL -u labx2; userdel -r labx2 ← 先杀掉他的进程
Ubuntu 上的 deluser –remove-home 需要完整的 perl 包,我这台云镜像没有,报 fatal: In order to use the –remove-home … you need to install the 'perl' package;装 perl,或者直接用 userdel -r。
11. 进程与服务
11.1 看进程
ps aux # BSD 风格,%CPU %MEM 一目了然
ps -ef # 带父进程号 PPID
ps aux –sort=-%mem | head # 按内存排序
ps aux –sort=-%cpu | head # 按 CPU 排序
ps -eo pid,ppid,user,%cpu,%mem,rss,etime,cmd –sort=-rss | head # 自己挑列
ps -p 1 -o pid,user,etime,lstart,cmd # 某个进程什么时候启动的
ps -C sshd -o pid,cmd # 按命令名
pgrep -a sleep # 按名字找 PID 并显示命令行
pidof sleep
pstree -p 1 | head # 进程树(psmisc 包)
ps aux | grep sleep 会把 grep 自己也搜出来,写成 grep "[s]leep" 就不会。
/proc/PID/ 下能看到进程的所有信息:
# cat /proc/$P/status | grep -E '^(Name|State|PPid|VmRSS|Threads)'
# ls -l /proc/$P/cwd /proc/$P/exe # 工作目录、可执行文件
# tr '\\0' ' ' < /proc/$P/cmdline # 完整命令行
11.2 top
top -b -n 1 | head -15 # 非交互输出一次(写脚本、存日志用)
top -b -n 1 -o %MEM # 按内存排
交互界面按 q 退出。负载看 uptime 或 /proc/loadavg,和 nproc(CPU 核数)对比。
11.3 杀进程
kill PID # 默认发 SIGTERM(15),让程序自己收尾退出
kill -9 PID # SIGKILL,强杀,程序没有机会收尾
kill -0 PID; echo $? # 不发信号,只检查进程还在不在:不在时 rc=1,No such process
killall sleep # 按名字杀全部(psmisc 包)
pkill -x sleep # 按名字精确匹配
pkill -f "关键字" # 按完整命令行匹配
pkill -KILL -u 用户 # 杀某个用户的所有进程
程序忽略了 SIGTERM 时,kill 杀不掉,只能 kill -9:
# bash -c 'trap "" TERM; sleep 600' & P2=$!; kill $P2; sleep 0.5; kill -0 $P2 && echo "SIGTERM 被忽略,还活着"
SIGTERM 被忽略,还活着
# kill -9 $P2
bash: line 1: 1493 Killed bash -c 'trap "" TERM; sleep 600' > /dev/null 2>&1
🔴 pkill -f 会杀掉你自己:它匹配的是完整命令行,如果你的关键字恰好出现在当前 shell / 脚本的命令行里(比如在一个 bash -c "… pkill -f 关键字 …" 里执行),它连执行 pkill 的那个 shell 一起杀。写这次测试脚本时我被它坑了好几次。把关键字写成 pkill -f "slee[p] 777" —— 正则匹配的还是 sleep 777,但命令行里的字面是 slee[p] 777,不会匹配到自己。
11.4 后台运行,退出登录也不停
nohup 命令 > /tmp/app.log 2>&1 & # 最常用:输出一定要重定向
setsid 命令 > /dev/null 2>&1 < /dev/null & # 彻底脱离当前会话(父进程变成 1)
nohup 不重定向时,输出写到当前目录的 nohup.out。
🔴 后台进程不重定向输出,会让远程执行一直不返回:ssh 服务器 '命令 &' 或者自动化工具里起后台进程,如果没有 > /dev/null 2>&1,后台进程占着 SSH 通道的输出,SSH 就一直等它结束。我写这次的测试脚本时卡住过几次,全是这个原因。
tmux:断线了还能回来接着看(要装 tmux):
tmux new -d -s lab '长时间运行的命令' # 后台新建一个会话
tmux ls # lab: 1 windows (created …)
tmux capture-pane -pt lab # 不进去,直接看它屏幕上的内容
tmux send-keys -t lab C-c # 往里面发 Ctrl+C
tmux kill-session -t lab
11.5 优先级
# nice -n 10 sleep 600 & P=$!; ps -o pid,ni,cmd -p $P
PID NI CMD
106701 10 sleep 600
# renice -n 15 -p $P
106701 (process ID) old priority 10, new priority 15
nice 值越小优先级越高。普通用户只能往低了调,调成负数会被拒绝:
# su -s /bin/sh nobody -c 'nice -n -5 true'
nice: cannot set niceness: Permission denied
11.6 systemctl 常用操作
三台的 systemd 版本:CentOS 7 是 219,Rocky 9 是 252,Ubuntu 24.04 是 255。
| 启动 / 停止 / 重启 | systemctl start / stop / restart 服务 |
| 重载配置(服务支持的话) | systemctl reload 服务;不确定就用 reload-or-restart |
| 看状态和最近日志 | systemctl status 服务 |
| 开机自启 / 取消 | systemctl enable / disable 服务;enable –now 顺便启动 |
| 是否在运行 / 是否自启 | systemctl is-active 服务 / is-enabled 服务 |
| 看 unit 文件内容 | systemctl cat 服务 |
| 看属性 | systemctl show 服务 -p MainPID -p ActiveState |
| 所有运行中的服务 | systemctl list-units –type=service –state=running |
| 所有失败的 | systemctl list-units –failed |
| 所有开机自启的 | systemctl list-unit-files –state=enabled |
| 禁止启动(比 disable 更彻底) | systemctl mask 服务 / unmask |
| 改了 unit 文件后 | systemctl daemon-reload |
服务名在不同系统上不一样:
| SSH | sshd | ssh(sshd 是别名,is-enabled sshd 返回 alias) |
| 定时任务 | crond | cron |
| 时间同步 | chronyd | systemd-timesyncd(我这台没装 chrony) |
# systemctl is-active sshd crond cron chronyd (CentOS 7)
active
active
unknown
active
systemctl is-system-running 在我这台 CentOS 上显示 degraded,因为有一个服务(network.service)开机就失败了;systemctl list-units –failed 能看到是哪个。
对不支持 reload 的服务执行 reload:Failed to reload labweb.service: Job type reload is not applicable for unit labweb.service.,退出码 3。
11.7 把自己的程序做成服务
# /etc/systemd/system/labweb.service
[Unit]
Description=Lab demo web
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/labweb
ExecStart=/usr/bin/python3 -m http.server 8090
Restart=on-failure
RestartSec=2
User=nobody
[Install]
WantedBy=multi-user.target
# systemctl daemon-reload && systemctl start labweb && systemctl status labweb –no-pager | head -6
● labweb.service – Lab demo web
Loaded: loaded (/etc/systemd/system/labweb.service; disabled; vendor preset: disabled)
Active: active (running) since Tue 2026-09-22 20:36:29 CST; 1s ago
Main PID: 106832 (python)
# systemctl enable labweb
Created symlink from /etc/systemd/system/multi-user.target.wants/labweb.service to /etc/systemd/system/labweb.service.
(上面是 CentOS 7 的输出,ExecStart 用的是 python -m SimpleHTTPServer 8090。Rocky 和 Ubuntu 会对 User=nobody 给一句警告:Special user nobody configured, this is not safe!,正式服务建一个专门的用户。)
Restart=on-failure 的效果:kill -9 之后被自动拉起,普通 kill 不会:
# P=$(systemctl show -p MainPID –value labweb); kill -9 $P; sleep 3
# systemctl show -p MainPID –value labweb # MainPID 变了
# journalctl -u labweb -n 4 (Rocky 9)
labweb.service: Main process exited, code=killed, status=9/KILL
labweb.service: Failed with result 'signal'.
labweb.service: Scheduled restart job, restart counter is at 1.
Started Lab demo web.
普通 kill(SIGTERM)之后 systemctl is-active labweb 是 inactive,没有拉起 —— on-failure 只管异常退出。想要无论如何都拉起,写 Restart=always,实测普通 kill 之后也被拉起了(active,PID 变了)。
🔴 CentOS 7 的 systemd 219 没有 –value,会报 systemctl: unrecognized option '–value'。三个系统通用的写法:
systemctl show -p MainPID labweb | cut -d= -f2
(CentOS 7 上 kill -9 后的日志是 labweb.service holdoff time over, scheduling restart.,同样被拉起了。)
🔴 改了 unit 文件,不 daemon-reload 就重启,用的还是旧配置:
# sed -i 's/8090/8091/' /etc/systemd/system/labweb.service; systemctl restart labweb
Warning: labweb.service changed on disk. Run 'systemctl daemon-reload' to reload units.
# ss -lnt | grep -oE ':809[01] '
:8090 ← 还是旧端口
# systemctl daemon-reload; systemctl restart labweb; ss -lnt | grep -oE ':809[01] '
:8091
不改原文件,加一段覆盖配置(drop-in):
mkdir -p /etc/systemd/system/labweb.service.d
printf '[Service]\\nEnvironment=LABVAR=hello\\n' > /etc/systemd/system/labweb.service.d/override.conf
systemctl daemon-reload && systemctl restart labweb
systemctl show labweb -p Environment -p DropInPaths
# Environment=LABVAR=hello
# DropInPaths=/etc/systemd/system/labweb.service.d/override.conf
改系统自带的服务(比如 sshd)用 systemctl edit 服务,效果一样,是连载第三篇的一节。
mask 自己写的服务会失败(因为 mask 要在 /etc/systemd/system/ 下放一个同名链接,而那里已经有你的文件了):
Failed to execute operation: Invalid argument # CentOS 7
Failed to mask unit: File /etc/systemd/system/labweb.service already exists. # Rocky 9 / Ubuntu
删掉服务:
systemctl stop labweb; systemctl disable labweb
rm -f /etc/systemd/system/labweb.service; rm -rf /etc/systemd/system/labweb.service.d
systemctl daemon-reload; systemctl reset-failed
服务起不来怎么查、Type= 选错、start-limit、环境变量为空这些,连载第三篇有整整十节。
11.8 看服务日志
journalctl -u labweb –no-pager -n 20 # 最近 20 行
journalctl -u labweb –since "10 min ago"
journalctl -u labweb -f # 实时跟
journalctl -u labweb -p err # 只看错误
journalctl -u labweb -o cat # 只要消息本身,不要时间和主机名
更多见第 13 章。
11.9 定时关机、重启
# shutdown -h +30 "维护"
Shutdown scheduled for Tue 2026-09-22 21:06:40 CST, use 'shutdown -c' to cancel.
# shutdown -c # 取消
CentOS 7 上取消后,/run/systemd/shutdown/scheduled 这个文件会晚一点才消失,别拿它判断有没有取消成功;日志里有 Shutdown canceled. 就是取消了。
last -x reboot shutdown | head # 历次开关机时间
who -b # 本次启动时间
uptime -s
systemctl get-default # 默认启动目标:multi-user.target(字符界面)
12. 磁盘:查空间、分区挂载、fstab、LVM、swap、NFS
分区、格式化、LVM 这些我没有动真实磁盘,都是用文件做的 loop 设备演示的(truncate -s 1G d1.img && losetup -fP –show d1.img),命令和对真实磁盘操作完全一样,把 /dev/loop0 换成你的 /dev/sdb 就行。
12.1 看空间
df -h # 各分区用量
df -hT # 带文件系统类型
df -i # inode 用量(文件数太多也会「满」)
df -h –output=source,fstype,size,used,avail,pcent,target -x tmpfs -x devtmpfs # 去掉内存文件系统
lsblk # 有哪些盘、怎么分的区、挂在哪
lsblk -f # 带文件系统类型和 UUID
blkid # 所有分区的 UUID、类型、卷标
我这台 CentOS 的根分区是 xfs(LVM),两台云镜像是 ext4:
# df -hT / (CentOS)
Filesystem Type Size Used Avail Use% Mounted on
/dev/mapper/centos-root xfs 26G 3.3G 23G 13% /
12.2 磁盘满了,找大头
du -sh /var/log /usr /etc # 几个目录各多大
du -h -d 1 /var | sort -rh | head # /var 下哪个子目录最大,逐层往下找
find / -xdev -type f -size +50M -exec ls -lh {} \\; 2>/dev/null | awk '{print $5, $9}' | sort -rh | head
journalctl –disk-usage # systemd 日志占了多少
journalctl –vacuum-size=100M # 只保留 100M
「df 说满了,du 却找不到」(文件删了但进程还开着)、「No space left 但 df 只用了 1%」(inode 用完)、「Size ≠ Used + Avail」(ext4 给 root 预留了 5%)这三个,连载第四篇都有复现和解法。
12.3 分区、格式化、挂载
parted -s /dev/loop0 mklabel gpt mkpart primary ext4 1MiB 50% mkpart primary 50% 100%
partprobe /dev/loop0 # 让内核重新读分区表
mkfs.ext4 -L labdata /dev/loop0p1
mkfs.xfs -L labxfs /dev/loop0p2
mkdir -p /mnt/lab1 /mnt/lab2
mount /dev/loop0p1 /mnt/lab1
mount /dev/loop0p2 /mnt/lab2
# lsblk /dev/loop0 (Rocky)
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
loop0 7:0 0 1G 0 loop
├─loop0p1 259:2 0 511M 0 part
└─loop0p2 259:3 0 511M 0 part
# df -hT /mnt/lab1 /mnt/lab2
# findmnt /mnt/lab1
⚠️ 对普通文件执行 mkfs.ext4,CentOS 7 会先问 Proceed anyway? (y,n),在脚本里就卡在那儿(新版本不问)。要格式化镜像文件,加 -F。
卸载时 target is busy:
# cd /mnt/lab1; umount /mnt/lab1; echo "rc=$?"
umount: /mnt/lab1: target is busy.
rc=32
# fuser -vm /mnt/lab1 (Rocky;fuser 在 psmisc 包里)
USER PID ACCESS COMMAND
/mnt/lab1: root kernel mount /mnt/lab1
root 2094 ..c.. bash ← 就是我自己的 shell 停在里面
# cd /; umount /mnt/lab1 ← 离开之后就能卸载了
lsof +D /mnt/lab1 也能查。
临时改成只读、改回读写:
mount -o remount,ro /mnt/lab1
mount -o remount,rw /mnt/lab1
挂镜像文件:mount -o loop 镜像文件 /mnt/img。
12.4 开机自动挂载:/etc/fstab
用 UUID,不用 /dev/sdb1(设备名重启后可能变):
# blkid -s UUID -o value /dev/loop0p1
03d5fd6b-8fe3-40d0-8451-bdb253769932
# echo "UUID=03d5fd6b-8fe3-40d0-8451-bdb253769932 /mnt/lab1 ext4 defaults,nofail 0 2" >> /etc/fstab
# mount -a ← 改完立刻用它验证,别等重启
# findmnt /mnt/lab1
findmnt –verify 检查整个 fstab(Rocky 9 / Ubuntu 有,CentOS 7 的 findmnt 不支持这个参数):
# echo "UUID=0000-bad /mnt/labbad ext4 defaults 0 2" >> /etc/fstab
# mount -a; echo "rc=$?"
mount: /mnt/labbad: can't find UUID=0000-bad.
rc=32
# findmnt –verify
[E] unreachable on boot required source: UUID=0000-bad
0 parse errors, 1 error, 1 warning
Ubuntu 还会提醒:your fstab has been modified, but systemd still uses the old version; use 'systemctl daemon-reload' to reload。
🔴 nofail 很重要:fstab 里有一行挂不上、又没写 nofail,开机会进紧急模式、SSH 连不上。连载第四篇在 Rocky 和 Ubuntu 上真的重启测过这个。改 fstab 前先 cp /etc/fstab /etc/fstab.bak。
12.5 文件系统检查、调整
e2fsck -f -y /dev/loop0p1 # ext4 检查,必须先卸载
resize2fs /dev/loop0p1 400M # ext4 缩小到 400M(先卸载、先 e2fsck)
tune2fs -l /dev/loop0p1 | grep -E 'Block count|Reserved block count'
tune2fs -m 1 /dev/loop0p1 # 预留给 root 的空间从 5% 改成 1%:Setting reserved blocks percentage to 1% (4096 blocks)
xfs_info /mnt/lab2 # xfs 看信息(挂着看)
xfs_repair -n /dev/loop0p2 # xfs 只检查不修,要先卸载
挂着的时候跑 xfs_repair:xfs_repair: /dev/vgx/lx contains a mounted and writable filesystem,直接拒绝。
xfs 只能扩不能缩,ext4 两个方向都行 —— 连载第四篇。
12.6 LVM
# pvcreate /dev/loop1 && vgcreate vglab /dev/loop1 && lvcreate -L 300M -n lvdata vglab -y
# pvs; vgs vglab; lvs vglab
# mkfs.ext4 /dev/vglab/lvdata && mount /dev/vglab/lvdata /mnt/lv
/dev/vglab/lvdata 和 /dev/mapper/vglab-lvdata 是同一个东西。
🔴 扩容分两步:扩 LV,再扩文件系统。只做第一步,df 不变:
# lvextend -L +200M /dev/vglab/lvdata
Logical volume vglab/lvdata successfully resized.
# df -h /mnt/lv | tail -1
/dev/mapper/vglab-lvdata 272M 14K 253M 1% /mnt/lv ← 没变
# resize2fs /dev/vglab/lvdata ← ext4 用 resize2fs
# df -h /mnt/lv | tail -1
/dev/mapper/vglab-lvdata 459M 14K 432M 1% /mnt/lv
xfs 的第二步是 xfs_growfs 挂载点(挂着做):
# lvextend -L +200M /dev/vgx/lx; xfs_growfs /mnt/x (Rocky,xfs)
Logical volume vgx/lx successfully resized.
data blocks changed from 76800 to 128000
一步到位:加 -r,顺便扩文件系统:
# lvextend -r -L +100M /dev/vglab/lvdata (Rocky)
Extended file system ext4 on vglab/lvdata.
Logical volume vglab/lvdata successfully resized.
# lvextend -r -l +100%FREE /dev/vglab/lvdata # 把 VG 剩下的全给它
删除:umount → lvremove -y vglab/lvdata → vgremove -y vglab → pvremove -y /dev/loop1。
云盘在控制台扩了容,分区和文件系统没跟着变大,要 growpart —— 连载第四篇。
12.7 swap 文件
dd if=/dev/zero of=/root/swap.lab bs=1M count=256
chmod 600 /root/swap.lab
mkswap /root/swap.lab
swapon /root/swap.lab
swapon –show; free -h | grep -i swap
echo '/root/swap.lab none swap defaults 0 0' >> /etc/fstab # 开机自动启用
swapon -a # 用 fstab 验证一下
关掉:swapoff /root/swap.lab,再从 fstab 删掉那一行、删文件。
🔴 在 CentOS 7(xfs)上用 fallocate 建的 swap 文件,swapon 会失败:
# fallocate -l 64M /root/swap2.lab; chmod 600 /root/swap2.lab; mkswap /root/swap2.lab; swapon /root/swap2.lab
swapon: /root/swap2.lab: swapon failed: Invalid argument
# dmesg | tail -1
[ 5411.561603] swapon: swapfile has holes
Rocky 和 Ubuntu(ext4)上 fallocate 建的没问题。要三个系统都能用,就用 dd。
cat /proc/sys/vm/swappiness 看内核多积极地用 swap(第 16 章)。
12.8 NFS 共享目录
服务端(Rocky 9):
# dnf install -y nfs-utils
# mkdir -p /srv/nfs/share
# echo '/srv/nfs/share 10.115.180.0/24(rw,sync,no_root_squash)' > /etc/exports
# systemctl enable –now nfs-server
# exportfs -rav
exporting 10.115.180.0/24:/srv/nfs/share
# firewall-cmd –permanent –add-service=nfs –add-service=rpc-bind –add-service=mountd; firewall-cmd –reload
客户端(Ubuntu 24.04):
# apt install -y nfs-common
# showmount -e 10.115.180.19
Export list for 10.115.180.19:
/srv/nfs/share 10.115.180.0/24
# mount -t nfs 10.115.180.19:/srv/nfs/share /mnt/nfs
# df -hT /mnt/nfs | tail -1
10.115.180.19:/srv/nfs/share nfs4 29G 1.7G 28G 6% /mnt/nfs
默认挂上的是 NFSv4.2。开机自动挂载,fstab 里写:
10.115.180.19:/srv/nfs/share /mnt/nfs nfs defaults,_netdev,nofail 0 0
🔴 服务端防火墙没放行时,客户端的表现是:
# showmount -e 10.115.180.19
clnt_create: RPC: Unable to receive
# mount -t nfs 10.115.180.19:/srv/nfs/share /mnt/nfs ← 一直卡住,不报错
放行 nfs、rpc-bind、mountd 三个服务后恢复。
root_squash:把导出选项从 no_root_squash 改成 root_squash 并 exportfs -ra 后,客户端的 root 再往那个目录(属主是服务端的 root)里写文件,报 touch: cannot touch …: Permission denied。no_root_squash 等于让客户端的 root 在共享目录里也是 root,生产环境慎用。
13. 日志
13.1 日志文件在哪
| 系统综合 | /var/log/messages | /var/log/syslog |
| 登录、sudo、SSH | /var/log/secure | /var/log/auth.log |
| 定时任务 | /var/log/cron | 在 syslog 里 |
| 内核 | dmesg / journalctl -k | /var/log/kern.log |
| 软件安装 | /var/log/yum.log(7)/ dnf.log(9) | /var/log/apt/history.log、/var/log/dpkg.log |
Ubuntu 24.04 的 syslog 时间戳改成了 ISO 格式:2026-09-22T12:44:51.366110+00:00 ubuntu2404 sshd[4081]: Accepted publickey for root …,老的 grep 'Sep 22' 写法会匹配不到。
13.2 journalctl
journalctl -n 20 # 最近 20 行
journalctl -f # 实时跟
journalctl -b # 本次开机以来
journalctl -b -1 # 上一次开机(需要持久化,见 13.3)
journalctl –list-boots
journalctl -u sshd # 某个服务(Ubuntu 是 -u ssh)
journalctl -p err -b # 本次开机的错误
journalctl –since "1 hour ago" –until now
journalctl –since today
journalctl -k # 内核消息
journalctl _PID=1 # 某个进程
journalctl -t labtag # 按标签
journalctl -o json-pretty -n 1 # 看一条日志的全部字段
脚本里写日志:logger -t labtag "hello",会同时进 journal 和 messages / syslog:
# logger -t labtag "hello from logger 1790081187"
# journalctl -t labtag -n 1
Sep 22 20:46:27 localhost.localdomain labtag[119512]: hello from logger 1790081187
# grep labtag /var/log/messages
/var/log/messages:Sep 22 20:46:27 localhost labtag: hello from logger 1790081187
13.3 重启后日志没了:journal 持久化
# grep -E '^#?Storage' /etc/systemd/journald.conf; ls -d /var/log/journal /run/log/journal
#Storage=auto
ls: cannot access /var/log/journal: No such file or directory ← CentOS 7 / Rocky 9
/run/log/journal
Storage=auto 的意思是「/var/log/journal 存在就持久化,不存在就放内存」。我这台 CentOS 和 Rocky 都没有这个目录,journal 只在 /run 里,重启就没了;Ubuntu 有这个目录,是持久的。要持久化:建目录再重启 journald,连载第六篇有完整步骤和重启验证。
13.4 dmesg
dmesg | tail # 内核日志,时间是开机后的秒数
dmesg -T | tail # 换成人看的时间:[Tue Sep 22 20:44:13 2026] XFS (loop0p2): Unmounting Filesystem
dmesg –level=err,warn
普通用户看不了 dmesg、OOM 杀进程去哪看,都在连载第六篇。
13.5 谁登录过、谁登录失败
last -n 5 # 成功登录
lastb -n 5 # 失败登录(读 /var/log/btmp,需要 root)
grep -E 'Accepted|Failed' /var/log/secure # RHEL 系
grep -E 'Accepted|Failed' /var/log/auth.log # Ubuntu
journalctl -u sshd | grep -E 'Accepted|Failed' # 都能用(Ubuntu 是 -u ssh)
Sep 22 20:44:12 localhost sshd[116720]: Accepted password for root from 192.168.3.65 port 60037 ssh2
Rocky 9 上 SSH 登录日志里的进程名是 sshd-session。
13.6 logrotate:日志轮转
谁触发的:CentOS 7 是 /etc/cron.daily/logrotate,Rocky 9 和 Ubuntu 是 logrotate.timer(systemctl list-timers | grep logrotate)。
给自己的程序日志配一个:
# cat /etc/logrotate.d/labapp
/var/log/labapp/*.log {
daily
rotate 3
compress
missingok
notifempty
copytruncate
dateext
}
# logrotate -d /etc/logrotate.d/labapp ← 预演,只打印要做什么
rotating pattern: /var/log/labapp/*.log after 1 days (3 rotations)
considering log /var/log/labapp/app.log
# logrotate -f /etc/logrotate.d/labapp ← 立即强制轮转一次
# ls /var/log/labapp/
app.log
app.log-20260922.gz
# wc -l < /var/log/labapp/app.log
0 ← copytruncate:原文件被清空,程序不用重开文件
⚠️ 用了 dateext,同一天只能轮转一次。当天再 -f 一次:
error: destination /var/log/labapp/app.log-20260922.gz already exists, skipping rotation (Rocky / Ubuntu)
(CentOS 7 上不报错,也不轮转。)
copytruncate 和 create 的区别、轮转后程序还往旧文件写,是连载第六篇的一节。
14. 定时任务
14.1 crontab
crontab -l # 看(没有时:no crontab for root,退出码 1)
crontab -e # 用 $EDITOR 打开编辑,保存退出后生效
crontab -l -u 用户 # 看别人的(root)
crontab -r # 🔴 删掉当前用户的全部定时任务,不确认
在脚本里追加一条,不进编辑器:
# (crontab -l 2>/dev/null; echo '*/1 * * * * date >> /tmp/labcron.log 2>&1') | crontab –
# crontab -l
*/1 * * * * date >> /tmp/labcron.log 2>&1
用户的 crontab 存在:RHEL 系 /var/spool/cron/用户名,Ubuntu /var/spool/cron/crontabs/用户名。不要直接编辑这些文件,用 crontab 命令。
时间格式:分 时 日 月 周。
*/5 * * * * 每 5 分钟
0 2 * * * 每天 2:00
30 1 * * 1 每周一 1:30
0 9 1 * * 每月 1 号 9:00
「日」和「周」同时写是「或」不是「且」—— 连载第三篇。
14.2 /etc/cron.d:系统级,多一列用户名
# cat /etc/cron.d/labjob
*/1 * * * * root echo "cron.d $(date +\\%T)" >> /tmp/labcrond.log
- 比用户 crontab 多一列用户名(上面的 root);
- 命令里的 % 要写成 \\%,否则后面的内容整个被当成标准输入 —— 连载第三篇有复现。
等一分钟后:
# tail -1 /tmp/labcrond.log
cron.d 20:47:01
14.3 执行了没有?看 cron 日志
# grep CMD /var/log/cron | tail -2 (CentOS 7 / Rocky 9)
Sep 22 20:47:01 localhost CROND[120342]: (root) CMD (echo "cron.d $(date +%T)" >> /tmp/labcrond.log)
Sep 22 20:47:01 localhost CROND[120343]: (root) CMD (date >> /tmp/labcron.log 2>&1)
# journalctl -u cron -n 5 (Ubuntu;也在 /var/log/syslog 里)
Sep 22 12:47:01 ubuntu2404 CRON[5226]: (root) CMD (date >> /tmp/labcron.log 2>&1)
日志里有 CMD 只说明 cron 启动了命令,命令本身成没成功要看它自己的输出 —— 所以命令一定要重定向输出到文件。
⚠️ cron 里的环境和你登录时不一样。我这台 CentOS 的系统 locale 是中文,cron 里 date 的输出就是中文:
# tail -1 /tmp/labcron.log (CentOS,cron 执行的 date)
2026年 09月 22日 星期二 20:47:01 CST
而 Rocky 和 Ubuntu 上是 Tue Sep 22 12:47:01 PM UTC 2026。脚本里要解析命令输出的,开头写上 export LANG=C。PATH 也不一样(命令找不到),连载第三篇有完整对比。
14.4 systemd timer:能看到下次什么时候执行
# /etc/systemd/system/labtick.service
[Unit]
Description=lab tick
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'date >> /tmp/labtick.log'
# /etc/systemd/system/labtick.timer
[Unit]
Description=lab tick timer
[Timer]
OnCalendar=*:*:0/20
AccuracySec=1s
Persistent=true
[Install]
WantedBy=timers.target
# systemctl daemon-reload && systemctl enable –now labtick.timer
# systemctl list-timers labtick.timer
NEXT LEFT LAST PASSED UNIT ACTIVATES
Tue 2026-09-22 20:47:40 CST 5s left n/a n/a labtick.timer labtick.service
# journalctl -u labtick.service -n 2
Sep 22 20:47:40 localhost.localdomain systemd[1]: Starting lab tick…
Sep 22 20:47:40 localhost.localdomain systemd[1]: Started lab tick.
OnCalendar=*:*:0/20 是每 20 秒(演示用)。常用写法:*-*-* 02:30:00 每天 2:30,Mon *-*-* 09:00 每周一 9 点。写完先验证表达式:
# systemd-analyze calendar "Mon *-*-* 09:00" (Rocky / Ubuntu)
Next elapse: Mon 2026-09-28 09:00:00 UTC
CentOS 7 的 systemd 219 没有这个子命令:Unknown operation 'calendar'。
停用:systemctl disable –now labtick.timer,再删掉两个文件、daemon-reload。
14.5 只执行一次:at / systemd-run
三台的 atd 我这边都没有运行(systemctl is-active atd 是 unknown / inactive)。在 Rocky 上装好再用:
# dnf install -y at; systemctl enable –now atd
# echo 'date > /tmp/at.out' | at now + 1 minute
warning: commands will be executed using /bin/sh
job 1 at Tue Sep 22 13:16:00 2026
# atq ← 排队中的任务
1Tue Sep 22 13:16:00 2026 a root
# atrm 1 ← 取消
不想装 at,用 systemd 自带的:
# systemd-run –on-active=5s –unit=labonce /bin/sh -c 'date > /tmp/once.out'
Running timer as unit: labonce.timer
Will run service as unit: labonce.service
# journalctl -u labonce.service -n 2
Sep 22 13:15:30 rocky9 systemd[1]: Started /bin/sh -c date > /tmp/once.out.
Sep 22 13:15:30 rocky9 systemd[1]: labonce.service: Deactivated successfully.
–on-active=10min、–on-active=2h 这样写更长的延时。
15. SSH
本章在 Rocky(客户端)和 Ubuntu(服务端)之间实测;CentOS 7 只看了默认配置,没有改动。
15.1 三台 sshd 的默认配置
sshd -T 打印 sshd 实际生效的全部配置(包括 sshd_config.d/ 里的覆盖),比看配置文件靠谱:
# sshd -T | grep -E '^(port|permitrootlogin|passwordauthentication|pubkeyauthentication) '
| permitrootlogin | yes | without-password(root 只能用密钥) |
| passwordauthentication | yes | yes |
| usedns | yes |
Rocky 9 的 permitrootlogin 也是 without-password。三台默认配置的完整对比在连载第五篇。
看主机指纹(第一次连接时会让你核对这个):
# ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub (CentOS)
256 SHA256:GQNERQrWgQxvbOIjeY/0vt35YFeA5hzHOhxH/VA3bRA no comment (ED25519)
15.2 密钥登录
生成密钥:
# ssh-keygen -t ed25519 -N '' -C 'lab@rocky' -f ~/.ssh/lab_ed25519
# ls -l ~/.ssh/lab_ed25519*
-rw——-. 1 root root 399 Sep 22 12:49 /root/.ssh/lab_ed25519
-rw-r–r–. 1 root root 91 Sep 22 12:49 /root/.ssh/lab_ed25519.pub
-N '' 是空口令(脚本、自动化用);给人用的密钥建议设口令,配合 ssh-agent。
把公钥放到服务器上:
# ssh-copy-id -i ~/.ssh/lab_ed25519.pub labs@10.115.180.200 (会问一次对方的密码)
…
Now try logging into the machine, with: "ssh -i /root/.ssh/lab_ed25519 -o 'StrictHostKeyChecking=no' 'labs@10.115.180.200'"
and check to make sure that only the key(s) you wanted were added.
# ssh -i ~/.ssh/lab_ed25519 labs@10.115.180.200 'ls -la ~/.ssh'
drwx—— 2 labs labs 4096 Sep 22 12:49 .
-rw——- 1 labs labs 91 Sep 22 12:49 authorized_keys
(我测试时加了 -o StrictHostKeyChecking=no、用 sshpass 自动输入密码,所以提示里带着这个参数。)ssh-copy-id 会把目录和文件权限设对(700 / 600)。手工放的话,也要保证这两个权限。
密钥登录失败时,看服务端日志(Ubuntu /var/log/auth.log,RHEL 系 /var/log/secure)。我把对方家目录改成 777 后:
# ssh -o BatchMode=yes u24 true (客户端)
labs@10.115.180.200: Permission denied (publickey,password).
# grep 'Authentication refused' /var/log/auth.log (服务端)
… sshd[6264]: Authentication refused: bad ownership or modes for directory /home/labs
家目录、~/.ssh、authorized_keys 任何一层对组或其他人可写,sshd 都会拒绝密钥 —— 连载第五篇把每一层都测了一遍。
看认证过程:ssh -v
# ssh -v u24 true 2>&1 | grep -E 'Offering|Server accepts|Authenticated'
debug1: Offering public key: /root/.ssh/lab_ed25519 ED25519 SHA256:N5xVACsDoIfmwIURLQjV4O3EkVXq7SOm0+y/F97fHjU explicit
debug1: Server accepts key: /root/.ssh/lab_ed25519 ED25519 SHA256:N5xVACsDoIfmwIURLQjV4O3EkVXq7SOm0+y/F97fHjU explicit
Authenticated to 10.115.180.200 ([10.115.180.200]:22) using "publickey".
ssh-agent:私钥有口令时,加进 agent 后就不用每次输入:
# eval $(ssh-agent -s); ssh-add ~/.ssh/lab_ed25519; ssh-add -l
Identity added: /root/.ssh/lab_ed25519 (lab@rocky)
256 SHA256:N5xVACsDoIfmwIURLQjV4O3EkVXq7SOm0+y/F97fHjU lab@rocky (ED25519)
15.3 ~/.ssh/config:给服务器起别名
# cat ~/.ssh/config
Host u24
HostName 10.115.180.200
User labs
Port 22
IdentityFile ~/.ssh/lab_ed25519
ServerAliveInterval 30
ServerAliveCountMax 3
ConnectTimeout 5
# ssh u24 'echo "别名登录 OK: $(whoami)@$(hostname)"'
别名登录 OK: labs@ubuntu2404
# scp /etc/hostname u24:/tmp/from_rocky # scp、rsync 也认这个别名
# ssh -G u24 | grep -E '^(hostname|user|identityfile)' # 看最终生效的参数
user labs
hostname 10.115.180.200
identityfile ~/.ssh/lab_ed25519
ServerAliveInterval 30:每 30 秒发一次心跳,防止空闲时被中间设备断开。
15.4 端口转发
本地转发 -L:服务器上的服务只监听 127.0.0.1(比如数据库管理页面),从本机访问它:
# ssh -f -N -L 18088:127.0.0.1:8088 u24 # 本机 18088 → 服务器上的 127.0.0.1:8088
# ss -lntp | grep 18088
LISTEN 0 128 127.0.0.1:18088 0.0.0.0:* users:(("ssh",pid=6180,fd=5))
# curl -s -o /dev/null -w 'L 转发 http=%{http_code}\\n' http://127.0.0.1:18088/
L 转发 http=200
远程转发 -R:让服务器能访问到我这边的服务(内网穿透的原理):
# ssh -f -N -R 17077:127.0.0.1:8077 u24 # 服务器上的 17077 → 我这边的 127.0.0.1:8077
# ssh u24 'curl -s -o /dev/null -w "R 反向转发 http=%{http_code}\\n" http://127.0.0.1:17077/'
R 反向转发 http=200
动态转发 -D:本机开一个 SOCKS5 代理,流量从服务器出去:
# ssh -f -N -D 11080 u24
# curl -s –socks5-hostname 127.0.0.1:11080 -o /dev/null -w 'D 动态 SOCKS http=%{http_code}\\n' http://www.baidu.com
D 动态 SOCKS http=200
-f 放到后台,-N 不执行远程命令。转发的端口默认只监听本机 127.0.0.1。
15.5 服务端加固
Rocky 9 和 Ubuntu 把改动放到 sshd_config.d/ 里(主配置开头有 Include /etc/ssh/sshd_config.d/*.conf);CentOS 7 的 OpenSSH 7.4 主配置里没有 Include(grep ^Include 查不到),直接改 /etc/ssh/sshd_config。改完先 sshd -t 检查,再 reload:
# cat /etc/ssh/sshd_config.d/10-lab.conf
PermitRootLogin no
PasswordAuthentication no
AllowUsers labs root
ClientAliveInterval 60
# sshd -t && echo "语法 OK"
语法 OK
# systemctl reload ssh # RHEL 系是 sshd
# sshd -T | grep -E '^(permitrootlogin|passwordauthentication|allowusers|clientaliveinterval) '
clientaliveinterval 60
permitrootlogin no
passwordauthentication no
allowusers labs
allowusers root
关掉密码登录后,强制用密码去连:
# ssh -o PubkeyAuthentication=no labs@10.115.180.200 true; echo "rc=$?"
labs@10.115.180.200: Permission denied (publickey).
rc=255
🔴 关密码登录之前,先确认密钥能登;改完别关当前窗口,另开一个窗口测试能登录,不行还能用老窗口改回来。
配置写错:
# sshd -t; echo "rc=$?"
/etc/ssh/sshd_config.d/10-lab.conf line 1: unsupported option "nooo".
rc=255
sshd -t 不通过就别 reload/restart。改 SSH 端口在 Rocky 上还要改 SELinux 端口标签和防火墙,在 Ubuntu 24.04 上要处理 ssh.socket,两个都是连载第五篇的专节。
15.6 连不上时看报错
ssh: connect to host 10.115.180.250 port 22: Connection timed out ← 机器不在 / 被防火墙丢包
ssh: connect to host 10.115.180.200 port 2222: Connection refused ← 机器在,但这个端口没人监听
labs@10.115.180.200: Permission denied (publickey). ← 连上了,认证失败
服务器重装后,客户端报 REMOTE HOST IDENTIFICATION HAS CHANGED,删掉旧的指纹记录:
# ssh-keygen -R 10.115.180.200
# Host 10.115.180.200 found: line 1
# Host 10.115.180.200 found: line 2
(是否该删,取决于你确认对方确实重装了;连载第五篇。)
16. 性能:CPU、内存、IO
16.1 先看整体
uptime # load average 三个数:1/5/15 分钟
nproc # CPU 核数,load 长期超过它就是排队了
free -h
vmstat 1 3 # 每秒一行,看 r(排队)、si/so(换页)、wa(IO 等待)
# free -h (Ubuntu)
total used free shared buff/cache available
Mem: 3.8Gi 424Mi 3.1Gi 4.7Mi 488Mi 3.4Gi
Swap: 0B 0B 0B
能用的内存看 available,不是 free。buff/cache 是可以回收的缓存(连载第六篇)。
# vmstat 1 3 (CentOS)
procs ———–memory———- —swap– —–io—- -system– ——cpu—–
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 2834292 3212 730708 0 0 16 77 125 104 1 1 98 0 0
第一行是开机以来的平均值,看第二行起。
16.2 sysstat:mpstat / iostat / pidstat / sar
要装 sysstat 包。
mpstat -P ALL 1 1 # 每个核分别多忙(单核跑满、总体只显示 25%,连载第六篇)
iostat -xz 1 2 # 每块盘的读写、await(响应时间)、%util
pidstat 1 1 # 每个进程的 CPU
sar -u 1 2 # CPU,sar 还能看历史
# iostat -xz 1 2 (Ubuntu,截取)
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s …
sda 0.00 0.00 0.00 0.00 0.00 0.00 92.00 106496.00 …
🔴 装了 sysstat 不一定在采集历史数据:我这台 Ubuntu 装完后 systemctl is-enabled sysstat 是 disabled,/var/log/sysstat/ 是空的;CentOS 7 上是 enabled,目录是 /var/log/sa/(Ubuntu 是 /var/log/sysstat/)。想用 sar 看昨天的数据,先确认它在采集(连载第六篇)。
16.3 谁在吃 CPU、谁在吃内存
top -b -n 1 | head -15
ps aux –sort=-%cpu | head
ps aux –sort=-%mem | head
我起一个 5 秒的死循环,top 和 ps 立刻能看到它占 100% 的一个核。
16.4 文件句柄、进程数限制
# ulimit -n; ulimit -u
1024
15325
# cat /proc/sys/fs/file-nr # 已分配 未用 上限
80009223372036854775807
/etc/security/limits.conf 改了,用 systemd 启动的服务不认,要在 unit 里写 LimitNOFILE= —— 连载第六篇。
16.5 内核参数
sysctl vm.swappiness net.core.somaxconn fs.file-max # 查
sysctl -w net.core.somaxconn=2048 # 临时改,重启失效
永久改:写到 /etc/sysctl.d/ 下,再加载:
# echo "net.core.somaxconn = 2048" > /etc/sysctl.d/99-lab.conf
# sysctl -p /etc/sysctl.d/99-lab.conf
net.core.somaxconn = 2048
# sysctl –system # 加载所有配置文件
三台的默认值差别不小:net.core.somaxconn CentOS 7 是 128,Rocky 9 和 Ubuntu 是 4096;vm.swappiness 我这台 CentOS 是 30、Ubuntu 是 60。网上抄的「优化参数」有些在新内核里已经不存在了,连载第六篇有实测。
16.6 粗测磁盘读写
# dd if=/dev/zero of=/root/io.test bs=1M count=200 oflag=direct # 写
209715200 bytes (210 MB) copied, 0.175652 s, 1.2 GB/s
# dd if=/root/io.test of=/dev/null bs=1M iflag=direct # 读
209715200 bytes (210 MB) copied, 0.133453 s, 1.6 GB/s
# rm -f /root/io.test
oflag=direct / iflag=direct 绕过缓存,否则测出来的是内存速度。这只是粗测,正经压测用 fio。iowait 高但 CPU 空着的排查,见连载第六篇。
17. SELinux(CentOS 7 / Rocky 9)与 AppArmor(Ubuntu)
17.1 看状态
# getenforce
Enforcing
# sestatus | head -6
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: enforcing
Mode from config file: enforcing
# grep ^SELINUX= /etc/selinux/config
SELINUX=enforcing
临时切换(重启恢复成配置文件里的值):
# setenforce 0; getenforce
Permissive ← 只记录不拦截,排查时用来确认「是不是 SELinux 挡的」
# setenforce 1; getenforce
Enforcing
永久关闭要改 /etc/selinux/config,不推荐;下面几节是正确的处理方法。
17.2 看标签
# ls -Z /etc/passwd
system_u:object_r:passwd_file_t:s0 /etc/passwd
# ps -eZ | grep sshd
system_u:system_r:sshd_t:s0-s0:c0.c1023 751 ? 00:00:00 sshd
# id -Z
unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
17.3 服务换了端口:加端口标签
要装 semanage(CentOS 7:policycoreutils-python;Rocky 9:policycoreutils-python-utils)。
# semanage port -l | grep -E '^(http_port_t|ssh_port_t|mysqld_port_t) '
http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000
mysqld_port_t tcp 1186, 3306, 63132-63164
ssh_port_t tcp 22
# semanage port -a -t http_port_t -p tcp 8899
# semanage port -l | grep ^http_port_t
http_port_t tcp 8899, 80, 81, 443, 488, 8008, 8009, 8443, 9000
# semanage port -d -t http_port_t -p tcp 8899 # 删除
nginx 改到 8888 端口起不来、SSH 改端口起不来,都是这个原因(连载第二、五篇有完整复现)。
17.4 文件标签不对:chcon 是临时的,semanage fcontext 才是永久的
网站放在 /srv/labweb,标签是 var_t,Web 服务读不了:
# ls -Z /srv/labweb/i.html
unconfined_u:object_r:var_t:s0 /srv/labweb/i.html
# chcon -t httpd_sys_content_t /srv/labweb/i.html; ls -Z /srv/labweb/i.html
unconfined_u:object_r:httpd_sys_content_t:s0 /srv/labweb/i.html
# restorecon -v /srv/labweb/i.html ← 一执行 restorecon(或全盘 relabel)就被改回去
Relabeled /srv/labweb/i.html from unconfined_u:object_r:httpd_sys_content_t:s0 to unconfined_u:object_r:var_t:s0
永久的写法:先登记规则,再 restorecon:
# semanage fcontext -a -t httpd_sys_content_t "/srv/labweb(/.*)?"
# restorecon -Rv /srv/labweb
Relabeled /srv/labweb from unconfined_u:object_r:var_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
Relabeled /srv/labweb/i.html from unconfined_u:object_r:var_t:s0 to unconfined_u:object_r:httpd_sys_content_t:s0
# semanage fcontext -l -C | grep labweb # 看自己加过的规则
/srv/labweb(/.*)? all files system_u:object_r:httpd_sys_content_t:s0
mv 过来的文件保留原来的标签(比如从 /root 移过来的是 admin_home_t),cp 过来的会继承新目录的 —— 这也是 403 的常见原因,连载第二篇。
17.5 布尔值:setsebool 一定加 -P
# getsebool httpd_can_network_connect
httpd_can_network_connect –> off
# setsebool -P httpd_can_network_connect on # 允许 Web 服务发起网络连接(反向代理要用)
# semanage boolean -l -C | grep httpd_can_network_connect
httpd_can_network_connect (on , on) Allow httpd to can network connect
括号里两个值:当前值、永久值。不加 -P 只改当前值,重启就回去了(连载第二篇重启实测过)。getsebool -a 列出全部,Rocky 9 上有 350 个。
17.6 到底拦了什么
ausearch -m avc -ts recent < /dev/null # 最近的拒绝记录
ausearch -m avc -ts today < /dev/null
sealert -a /var/log/audit/audit.log # 翻译成人话(setroubleshoot-server 包),很慢
🔴 在脚本里调用 ausearch 一定加 < /dev/null。它在非交互环境下会去读标准输入,一直卡住 —— 这次写测试脚本又被卡了一次。
没有拒绝记录时:ausearch 输出 <no matches>,sealert 输出 found 0 alerts in /var/log/audit/audit.log。
audit2allow 会把日志里的所有拒绝一起放行,用之前先看清楚它放行了什么(连载第二篇)。
17.7 AppArmor(Ubuntu)
# aa-status | head -3
apparmor module is loaded.
112 profiles are loaded.
18 profiles are in enforce mode.
# systemctl is-active apparmor
active
规则在 /etc/apparmor.d/。哪些程序有 profile、MySQL 数据目录挪走后起不来怎么办,在连载第二篇。
18. 时间时区、中文、脚本常见报错,以及报错速查
18.1 时间和时区
# timedatectl (CentOS 7)
Local time: Tue 2026-09-22 20:51:57 CST
Universal time: Tue 2026-09-22 12:51:57 UTC
RTC time: Tue 2026-09-22 12:51:57
Time zone: Asia/Shanghai (CST, +0800)
NTP enabled: yes
NTP synchronized: yes
两台云镜像默认是 UTC。改时区:
# timedatectl list-timezones | grep Asia/Shanghai
Asia/Shanghai
# timedatectl set-timezone Asia/Shanghai; date
Tue Sep 22 20:58:03 CST 2026
# ls -l /etc/localtime
lrwxrwxrwx. 1 root root 35 Sep 22 20:58 /etc/localtime -> ../usr/share/zoneinfo/Asia/Shanghai
我这台 Rocky 云镜像一开始连 /etc/localtime 都没有(ls: cannot access '/etc/localtime'),set-timezone 会把它建出来。
只想临时看另一个时区的时间:TZ=UTC date。
时间同步:CentOS 7 和 Rocky 9 用 chrony,Ubuntu 24.04 用 systemd-timesyncd:
# chronyc tracking | head -3 (CentOS / Rocky)
Reference ID : 771CCEC1 (119.28.206.193)
Stratum : 3
# chronyc sources -v | tail -2
^* 119.28.206.193 2 6 355 45 +5145us[+4555us] +/- 40ms
^- ntp.wdc2.us.leaseweb.net 3 6 377 46 -3598us[-3598us] +/- 147ms
# timedatectl timesync-status | head -2 (Ubuntu)
Server: 185.125.190.57 (ntp.ubuntu.com)
Poll interval: 4min 16s (min: 32s; max 34min 8s)
^* 是当前正在用的源。chrony 的服务器配置在 /etc/chrony.conf(Rocky 默认是 pool 2.rocky.pool.ntp.org iburst)。timedatectl timesync-status 只对 timesyncd 有效:CentOS 7 上报 Unknown operation timesync-status,Rocky 上报 Failed to query server: The name is not activatable。
时间差了 8 小时、日志时间和 date 对不上,连载第九篇有一节。
18.2 中文显示、报错变中文
# localectl status | head -2 (CentOS,我这台)
System Locale: LANG=zh_CN.UTF-8
VC Keymap: cn
# locale -a | grep -i zh_CN # 系统里有没有中文 locale
我这台 Rocky 云镜像默认没有中文 locale,装上语言包后,报错信息就变成中文了:
# dnf install -y glibc-langpack-zh
# LANG=zh_CN.UTF-8 ls /nosuch
ls: 无法访问 '/nosuch': 没有那个文件或目录
# LANG=zh_CN.UTF-8 date
2026年 09月 22日 星期二 12:58:10 UTC
Ubuntu 上是 locale-gen zh_CN.UTF-8(输出 Generation complete.)。
🔴 LC_ALL 会盖过 LANG:设了 LC_ALL 时,再怎么改 LANG 都不生效:
# env -u LC_ALL LANG=zh_CN.UTF-8 ls /nosuch
ls: 无法访问 '/nosuch': 没有那个文件或目录
# LC_ALL=en_US.UTF-8 LANG=zh_CN.UTF-8 ls /nosuch
ls: cannot access '/nosuch': No such file or directory
我的测试脚本一开始就因为导出了 LC_ALL,在 CentOS 和 Ubuntu 上怎么设 LANG 都还是英文。报错语言、排序规则「改了不生效」,先 locale 看一眼 LC_ALL 有没有值。
反过来,写脚本、贴报错给别人看时,用 LANG=C 让输出变成英文,方便搜索。cron 里的 locale 和你登录时的不一定一样(第 14 章)。
18.3 脚本报错
bad interpreter:脚本是 Windows 上写的(CRLF 换行)
# ./crlf2.sh
bash: /tmp/crlf2.sh: /bin/bash^M: bad interpreter: No such file or directory
# bash crlf2.sh | cat -A
hi^M$ ← 用 bash 执行能跑,但每行都带着 \\r
修:sed -i 's/\\r$//' 脚本 或 dos2unix 脚本(第 6 章)。
command not found:脚本就在当前目录
# labrun.sh; echo "rc=$?"
bash: labrun.sh: command not found ← 当前目录不在 PATH 里
rc=127
# ./labrun.sh
bash: ./labrun.sh: Permission denied ← 没有执行权限
# chmod +x labrun.sh && ./labrun.sh
run-ok
# bash labrun.sh ← 不加执行权限也能这样跑
run-ok
command not found:装在别的目录的命令
# mytool
bash: mytool: command not found
# export PATH=$PATH:/opt/labbin; mytool
mytool
要永久生效,把 export 那行写进 ~/.bashrc(只对自己)或 /etc/profile.d/xxx.sh(所有人)。/etc/profile.d/ 只在登录 shell 里加载:我放进去之后,bash -lc 里能看到新的 PATH,bash -c 里看不到 —— 所以要重新登录才生效。命令真没装的话,查第 3 章的对照表。
退出码 127 = 找不到命令,126 = 找到了但不能执行(没权限),1 / 2 一般是命令自己报的错。
18.4 报错速查表
左边是报错原文(可以直接 Ctrl+F 搜),右边是原因和去哪一章看。全部来自本文或连载的实测。
| curl: (7) … No route to host | firewalld 拒绝(不是路由问题) | 2.1 |
| curl: (28) Connection timed out | ufw / iptables 丢包 | 2.1 |
| Connection refused | 端口没人监听,或服务只监听 127.0.0.1,或规则是 reject | 2.1、9.6 |
| ERROR: Bad port | ufw 端口段要用冒号 9000:9010 | 2.4 |
| ufw 显示 Aborted 但退出码 0 | 没加 –force,脚本里读不到 y | 2.4 |
| package iptables-services is not installed(明明装了) | Rocky 9 上实际包名是 iptables-nft-services | 2.5 |
| 装 iptables-persistent 后 ufw 没了 | 两者互斥(Breaks) | 2.5 |
| E: Unable to locate package | 没 apt update,或本地 deb 没写 ./ | 3.3、3.5 |
| No matches found(yum provides) | CentOS 7 要写 '*/bin/命令' | 3.4 |
| Repository … is listed more than once / HTTPS Error 403 | 重复或失效的源拖慢所有 yum 命令 | 3.11 |
| cp: -r not specified; omitting directory | 复制目录要 -r | 5.2 |
| rm: invalid option — '.' | 文件名以减号开头,用 rm — 文件 | 5.4 |
| Operation not permitted(root 也删不掉) | 有 chattr +i | 5.9 |
| find -name *.conf 什么都搜不到 | 通配符没加引号,被 shell 提前展开 | 5.10 |
| gzip: invalid option — 'k' | CentOS 7 的 gzip 太老,用 gzip -c > 文件 | 7.2 |
| tar: Cannot update compressed archives | 压缩包不能追加 | 7.1 |
| tar: unrecognized option '–zstd' | CentOS 7 的 tar 1.26 不支持 | 7.1 |
| 解压 Windows 的 zip 中文乱码 | unzip -O GBK | 7.4 |
| scp: local "…" is not a regular file | 传目录要 -r | 8.2 |
| scp: stat local "22" | 端口参数是大写 -P | 8.2 |
| rsync: connection unexpectedly closed … (code 12) | 对端没装 rsync | 8.3 |
| curl 下载 404 却返回 0、文件里是 HTML | 没加 -f | 8.5 |
| curl: (60) SSL certificate problem | 证书不受信任(自签名等) | 8.5 |
| UNPROTECTED PRIVATE KEY FILE / Bad permissions(Windows) | 私钥 ACL 太宽,用 icacls 收紧 | 8.10 |
| Name or service not known | 域名解析失败(ping 退出码 2) | 9.4 |
| Error: NetworkManager is not running. | 这台机器不用 NM 管网络 | 9.10 |
| Permissions for /etc/netplan/… are too open | netplan 文件要 600 | 9.10 |
| netplan apply 后断了几秒 | DHCP 重新获取 | 9.10 |
| useradd: user '…' already exists | 用户已存在(退出码 9) | 10.3 |
| This account is currently not available. | 用户 shell 是 nologin | 10.5 |
| /var/tmp/sclXXXX: line 8: -u: command not found | devtoolset 的 sudo 包装盖住了系统 sudo | 10.5 |
| You are required to change your password immediately | 设过 chage -d 0 | 10.8 |
| >>> … syntax error near line 1 <<< | sudoers 写错,visudo -c 查出来的 | 10.9 |
| userdel: user … is currently used by process | 先杀掉该用户的进程 | 10.11 |
| nice: cannot set niceness: Permission denied | 普通用户不能提高优先级 | 11.5 |
| systemctl: unrecognized option '–value' | CentOS 7 的 systemd 219 太老 | 11.7 |
| Warning: … changed on disk. Run 'systemctl daemon-reload' | 改了 unit 文件没 reload | 11.7 |
| Job type reload is not applicable | 这个服务不支持 reload | 11.6 |
| Failed to mask unit: File … already exists | 不能 mask /etc/systemd/system 下的自建服务 | 11.7 |
| umount: … target is busy. | 有进程(包括你自己的 shell)在里面 | 12.3 |
| mount: … can't find UUID= | fstab 里的 UUID 不存在 | 12.4 |
| Proceed anyway? (y,n) 卡住 | CentOS 7 对文件 mkfs.ext4,加 -F | 12.3 |
| swapon failed: Invalid argument / swapfile has holes | xfs 上 fallocate 的 swap 文件,改用 dd | 12.7 |
| xfs_repair: … contains a mounted and writable filesystem | 先卸载 | 12.5 |
| clnt_create: RPC: Unable to receive | NFS 服务端防火墙没放行 | 12.8 |
| destination … already exists, skipping rotation | logrotate 用了 dateext,同一天只能转一次 | 13.6 |
| cron 里命令没执行 / 输出中文 | cron 的环境(PATH、LANG)和登录时不同;% 要转义 | 14.2、14.3 |
| Unknown operation 'calendar' | CentOS 7 的 systemd 太老 | 14.4 |
| Authentication refused: bad ownership or modes | 家目录或 .ssh 权限太宽 | 15.2 |
| Permission denied (publickey). | 密钥不对,或服务端关了密码登录 | 15.5、15.6 |
| unsupported option "…"(sshd -t) | sshd 配置写错 | 15.5 |
| ausearch 卡住不动 | 脚本里要加 < /dev/null | 17.6 |
| bad interpreter: /bin/bash^M | Windows 换行 | 18.3 |
| command not found(退出码 127) | 不在 PATH,或当前目录的脚本没写 ./ | 18.3 |
| Permission denied(执行脚本) | 没有执行权限 | 18.3 |
18.5 和连载的对应关系
这篇讲「怎么做」,连载讲「为什么这么做不生效」:
| 第一篇 | 认系统、装软件、CentOS 7 源失效、换国内源 | 1、3 |
| 第二篇 | 端口不通排查:firewalld / ufw / iptables / SELinux / AppArmor | 2、17 |
| 第三篇 | systemd 服务起不来、cron 不执行 | 11、14 |
| 第四篇 | 磁盘满了、LVM 扩容、fstab 写错 | 12 |
| 第五篇 | hosts / DNS、SSH 密钥和改端口、nmcli / netplan | 9、15 |
| 第六篇 | 日志没了、OOM、logrotate、内存与性能 | 13、16 |
| 第七篇 | 用户、组、sudo、权限、链接、删除 | 5、10 |
| 第八篇 | grep / sed / tar / zip / rsync / scp / curl | 6、7、8 |
| 第九篇 | 重定向、sudo 写文件、通配符、时间,以及报错速查 | 4、18 |

