欢迎光临
我们一直在努力

Linux 常用操作怎么做?开端口、换源、传文件、加用户、挂磁盘、写服务、定时任务 —— CentOS 7 / Rocky 9 / Ubuntu 24.04 三系统实测手册

/* 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 三个系统的对照

要做的事CentOS 7Rocky 9Ubuntu 24.04
刷新软件源索引 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

下面这张表是我在三台上逐个装过、确认命令能用的结果。「—」表示没找到对应的包。

命令CentOS 7Rocky 9Ubuntu 24.04
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
mail 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

服务名在不同系统上不一样:

CentOS 7 / Rocky 9Ubuntu 24.04
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 日志文件在哪

内容CentOS 7 / Rocky 9Ubuntu 24.04
系统综合 /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) '

CentOS 7(OpenSSH 7.4)Ubuntu 24.04(我这台)
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
赞(0)
未经允许不得转载:171主机测评 » Linux 常用操作怎么做?开端口、换源、传文件、加用户、挂磁盘、写服务、定时任务 —— CentOS 7 / Rocky 9 / Ubuntu 24.04 三系统实测手册
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址