一、开始敲命令之前:终端与帮助系统
在 Linux 中,学会「查命令」比背命令更重要。打开终端准备敲命令之前,有几个「不查不好意思问」的基础问题:眼前这行提示符是什么意思、忘了参数去哪查、为什么同一个 sh 在不同机器上行为不一样。这一章把这几件事说清楚,后面 16 章就能直接跳着查。
本手册的定位是速查:每个命令一小节,先给最常见的可直接复制的写法,再解释参数,最后补一句注意事项。遇到破坏性命令(rm -rf、mkfs、dd、kill -9)会单独标出风险。
1. 先看提示符:你是谁、在哪、什么身份
| 观察点 | 长这样 | 含义 |
|---|---|---|
| 普通用户提示符 | deploy@server:~$ | 用户 deploy、主机 server、当前目录 ~、$ 表示非 root |
| root 提示符 | root@server:/srv/app# | 结尾是 #,此刻敲的命令不再需要 sudo |
只有 bash-4.4$ | 容器里常见 | $PS1 没配置,通常是精简镜像,功能可能缺 |
| 续行提示符 | > | 引号或括号没闭合,Ctrl-C 放弃当前输入 |
| 光标处 | ^(vi 里) | 光标在 ^ 那个字符上,不在它后面 |
本手册示例统一假设:用户 deploy、主机 server、工作目录 /srv/app 或 /home/deploy、Shell 是 bash。行首的 $ 是提示符的一部分,复制到终端时不要把它带上。
2. man —— 查看命令手册页
man 是 manual(手册)的缩写,是 Linux 最权威的查命令方式。
示例 1:查看 ls 的手册页
man ls # 打开 ls 的手册页
打开后:q 退出、/关键词 搜索、n 跳到下一个匹配、空格翻页。
示例 2:指定章节编号
man 1 printf # 1=用户命令,5=文件格式,8=管理命令
man -k printf # 按关键词反查相关命令(等价于 apropos printf)
同一个名字在不同章节可能有多个手册页(比如 printf 既是命令又是 C 函数),man -a printf 可以一次看完所有同名章节。
示例 3:只要一行说明
whatis grep # 只输出一行描述:grep pattern lines matched
apropos firewall # 反过来:按关键词找命令名,能列出 rsync、dump 之类
3. --help 与 tldr —— 更快的两种查法
示例 1:选项速查
ls --help # 一屏列出全部选项,日常最常用,够用了
ls --version # 看命令版本与所属包,GNU 核心工具会写 ls (GNU coreutils) 9.x
示例 2:只看真实示例
tldr ls # 需要装 tldr 包;只给真实用法示例,比 man 快十倍
示例 3:超长的 GNU 手册
info coreutils 'ls invocation' # 层级导航:u 上级、n/p 前后节点
实用结论:查「某个参数是否存在、拼写对不对」用 --help 就够了;要看退出码、示例、相关文件用 man;--help 只讲参数,不讲「我该用哪个命令」,后者靠 apropos 和搜索引擎。
4. type、which、command —— 这条命令到底从哪来
Linux 的「命令」其实分三种类型,搞清类型能解释一大半「找不到命令」和「行为不对」:
| 类型 | 例子 | type 的输出 | 说明 |
|---|---|---|---|
| Shell 内建命令 | cd echo export source alias | cd is a shell builtin | 由 Shell 进程自己实现,改不了当前目录这类事的只能做成内建 |
| 外部命令(可执行文件) | ls grep systemctl | grep is /usr/bin/grep | 在 PATH 里找文件,fork 子进程执行 |
| 函数 / 别名 | 自定义 ll()、alias rm='rm -i' | ll is a function | 只在当前 Shell 生效,脚本里看不到 |
示例 1:排查「为什么我的 ls 带颜色」
type -a ls # 把别名/函数/内建/文件全部来源列出来
示例 2:脚本里的标准写法
command -v python3 # 只返回可执行路径;if ! command -v python3 >/dev/null; then …; fi
which curl # 只在 PATH 找文件(外部命令),看不到内建与别名,脚本别用它
whereis nginx # 顺带给出二进制、源码、man 页位置,装完不知道手册在哪时用
ls 这类命令既是外部命令又常被别名覆盖,交互式排查首选 type -a,写脚本判断用 command -v。
5. 发行版差异会体现在哪
同一句命令在不同发行版上可能失效,差异集中在四个地方:
| 方面 | Debian/Ubuntu | RHEL/Rocky/Fedora | Alpine |
|---|---|---|---|
| 包管理 | apt、dpkg | dnf/yum、rpm | apk |
| C 库与命令风格 | GNU coreutils | GNU coreutils | BusyBox(ls、find、sed 参数是精简版) |
sudo | 默认可用 | 默认可用 | 默认没有,需 apk add sudo |
| 服务管理 | systemctl(容器内常缺) | systemctl | 多用 OpenRC 或 supervisord |
| 防火墙后端 | ufw 常见 | firewalld 常见 | iptables/nftables 裸用 |
| Shell 默认 | bash | bash | ash(sh 指向 BusyBox ash) |
head -n 这类基础参数四处一致;差异大的是 sed -i(BusyBox 要求 -i '' 或干脆不支持同时给后缀)、date -d(BusyBox 只支持少数格式)、find -printf(BusyBox 无)。写跨发行版的脚本时,第一行用 #!/bin/sh 就别用 bash 语法,用 #!/bin/bash 就要确认机器上有 bash。
踩坑提醒
man页在容器里经常是空的(没装man和手册包):apt install man-db manpages,或临时靠--help与tldr。- 帮助文本分「BSD 版」与「GNU 版」两套,macOS 的
sed -i、date -j与 Linux 不通用,别把网上抄的 macOS 写法放到服务器上。 --help只讲参数,不讲「我该用哪个命令」。后者靠apropos和搜索引擎。
二、文件与目录:ls、cd、cp、mv、rm、ln
日常一半的终端时间花在「这个目录里有什么」「把这个文件挪到那里」。这一章的命令都写成可以直接复制粘贴的形式,重点在于看清 ls -l 的每个字段、理解复制与移动的差别,以及明白 rm -rf 为什么不能随手敲。
1. ls —— 列出目录内容
ls 是 Linux 中使用频率最高的命令之一,用于列出目录中的文件。
示例 1:列出详细信息(最常用)
ls -lh /srv/app # -l 长格式列出,-h 文件大小以 K/M/G 人读格式显示
输出长这样,十个字符的权限串是本章唯一需要「背」的东西:
$ ls -l /srv/app
-rw-r--r-- 1 deploy deploy 1.2K Oct 4 09:12 app.py
drwxr-xr-x 3 deploy deploy 4.0K Oct 4 09:20 static
lrwxrwxrwx 1 deploy deploy 11 Oct 4 09:21 current -> releases/v7
-rwsr-xr-x 1 root root 144.0K Mar 14 2024 helper
| 位置 | 内容 | 含义 |
|---|---|---|
| 第 1 字符 | - d l c b s p | 类型:普通文件 / 目录 / 链接 / 字符设备 / 块设备 / socket / 管道 |
| 第 2-4 | rwx | 属主(第三列的 deploy)的权限 |
| 第 5-7 | r-- | 属组(第四列)的权限 |
| 第 8-10 | r-- | 其他人的权限 |
| 第 2/5/8 位 | s 或 S | setuid(属主位)/ setgid(属组位);大写 S 表示「有标记但没有对应 x」 |
| 最后一位 | t 或 T | sticky bit,常见于 /tmp(drwxrwxrwt) |
| 第 2 列 | 1 / 3 | 硬链接数;目录至少 2(. 和 ..),所以看到 1 说明不是目录 |
| 第 6 列 | 1.2K / 4.0K | 文件字节数;目录显示的是它自己的 inode 表大小,不含内容 |
| 第 7 列 | Oct 4 09:12 | mtime(内容最后改动时间);ls -lu 看 atime,ls -lc 看 ctime(元数据变更) |
示例 2:显示隐藏文件
ls -la # -a 包含以 . 开头的隐藏文件;不加时不显示
示例 3:按不同方式排序
ls -lt # -t 按修改时间,最新在前
ls -lS # -S 按大小
ls -lr # -r 反向排序
ls -lX # -X 按扩展名
示例 4:方便脚本与管道
ls -1 # -1 单列输出,方便接管道
ls -Q # -Q 每个名字加引号,含空格的文件名安全
ls --time-style=long-iso # 日期变成 2026-10-04 09:12,脚本里更好解析
注意:ls 无参数时隐藏文件不显示;目录显示的大小不含内容,看总占用要用 du -sh。
2. pwd —— 打印当前目录
示例 1:我在哪
pwd # 打印当前工作目录的绝对路径
pwd -P # -P 解析掉所有符号链接,给出真实路径
Shell 有个同名内建的 pwd,pwd -P 走的是外部命令版本。
3. cd —— 切换目录
示例 1:几种常用跳法
cd /srv/app # 绝对路径
cd ~ # 回用户主目录(cd 不带参数效果相同)
cd .. # 上一层
cd - # 回到上一个待过的目录
注意:cd 是内建命令,只对当前 Shell 生效——在子 shell(比如脚本、( … ))里 cd 改不了父目录。
4. mkdir 与 rmdir —— 建目录、删空目录
示例 1:创建多级目录
mkdir build # 建单层目录,父目录不存在会失败
mkdir -p a/b/c # -p 逐级创建,已存在也不报错
mkdir -p logs/{2026..2028} # 花括号展开,一次建三年目录
示例 2:删除目录
rmdir build # 只能删空目录,非空就报错——比 rm -r 安全得多
5. touch —— 创建空文件或更新时间戳
示例 1:两个用途
touch app.log # 文件不存在就建空的;存在就更新 mtime
touch -d "2026-10-01" f # -d 指定时间戳
部署时常用 touch「戳一下」源文件触发增量构建。
6. tree —— 树状展示目录
示例 1:限制深度
tree -L 2 # -L 2 只显示两层,目录巨大时防止刷屏
tree -aI '__pycache__' # -a 含隐藏文件,-I 用正则排除
注意:tree 可能未安装(apt install tree)。
7. cp —— 复制
复制 = 原件保留、再拷贝一份。最常用的就两种写法:
示例 1:只复制内容
cp app.py /tmp/app.py # 目标权限按 umask 重算,属主变成执行者;生产备份别用这个
示例 2:完整备份(保留属性)
cp -a src/ backup/ # -a = -rp --preserve=links,timestamps:递归 + 保权限/时间戳/链接
示例 3:防覆盖
cp -i a.txt /tmp/ # -i 目标已存在时先问;脚本里建议加,或直接先判断存在
跨机复制目录不要直接用 cp,用 rsync -a src/ host:/srv/app/(见第十三章),它保留属性且可断点续传。
8. mv —— 移动与改名
示例 1:改名 / 移动
mv a.txt old/a_2026-10-04.txt # 同分区改名是改目录项,瞬时完成
mv big.iso /mnt/usb/ # 跨分区是先复制再删,慢且占双份空间
示例 2:控制覆盖行为
mv -n a b # -n 目标存在就不覆盖
mv -b a b # -b 覆盖前先把目标备份一份
9. rm —— 删除(危险命令,先读三条铁律)
rm 删除是不可逆的:它直接 unlink inode,文件系统层面没有回收站;debugfs/photorec 抢救成功率低且需要立刻停止写入。
示例 1:安全地删
rm -i notes.txt # -i 每个文件都确认
rm -I build/ # -I 只在删超过 3 个文件或递归时问一次,日常推荐
rm -rf build/ # -r 递归、-f 强制 —— 不可逆,谨慎
示例 2:删除前先看一眼范围
find build -type f -print # 确认范围再动手,比事后找回容易;确定无误后把 -print 换成 -delete
rm -rf 的三条铁律:
- 路径必须先补全再回车。
rm -rf $DIR/在$DIR为空时变成rm -rf /(新版 coreutils 会拒绝,但rm -rf /home/deploy/不会)。安全写法:rm -rf "${DIR:?变量未设置}/"*。 - 不要在
cd失败的目录里执行。脚本里写cd /srv/app && rm -rf cache用&&,别用;——cd失败时;后面的命令会在当前目录执行。 - 保护参数:
--preserve-root(默认开启,防rm -rf /);手生时永远先加-i。
10. ln —— 软链接与硬链接
Linux 的链接分两种,用法和性质完全不同:
| 维度 | 软链接(symbolic) | 硬链接(hard) |
|---|---|---|
| 创建 | ln -s /srv/app/releases/v7 current | ln big.txt big.bak |
| 本质 | 独立 inode,存的是路径字符串 | 同一 inode 的第二个目录项 |
| 目标删了 | 变成悬空链接,ls -l 显示红色 | 内容仍在,链接数减 1;归零才真删数据 |
| 跨文件系统 | 可以(存路径) | 不可以 |
| 指向目录 | 可以 | 一般禁止(root 也不建议) |
| 权限位 | lrwxrwxrwx(固定,实际权限看目标) | 与目标共享权限 |
示例 1:发布目录的经典用法
ln -s /srv/app/releases/v7 current # -s 建软链接,「版本目录 + current 指针」的发布方式
ln -sfn /srv/app/releases/v8 current # 切换版本:-f 覆盖已存在的同名链接,-n 把它当普通文件处理
注意:-n 不能省——否则 ln -s 新目标 current 会把新链接建到 current/ 目录里面去,这是最经典的坑。
11. 路径的三种写法与通配符
| 写法 | 例子 | 展开时机 |
|---|---|---|
| 绝对路径 | /srv/app/app.py | 永远有效,脚本里首选 |
| 相对路径 | ./static/logo.png、../../logs/app.log | 依赖当前目录,cd 后就变了 |
| 主目录 | ~/downloads/a.zip(~ 在最前才生效) | Shell 展开为 $HOME |
通配符(glob)与花括号展开:
| 模式 | 匹配 | 备注 |
|---|---|---|
*.log | 所有以 .log 结尾 | 没有匹配时 bash 默认原样保留该字符串(导致奇怪报错);shopt -s nullglob 让它展开为空 |
app?.py | 单字符占位 | app12.py 不匹配 |
[0-9]* | 字符集开头 | [!0-9]* 或 [^0-9]* 取反 |
**/*.js | 递归 | 需要 shopt -s globstar extglob |
{a,b}.conf | a.conf b.conf | 花括号展开发生在通配之前,适合批量建文件 |
file_{1..5}.tmp | 5 个名字 | mkdir -p logs/{2026..2028} 一次建三年目录 |
空格与特殊字符的文件名永远用引号包:cp "my report 2026.pdf" /tmp/。确认目录里有带换行的怪文件时用 ls -Q 或 find . -print0。
踩坑提醒
ls -l看到目录大小 4.0K 不代表里面只有 4KB,看总占用用du -sh 目录(第十四章)。cp到已存在的同名文件会静默覆盖,脚本里加-i或先判断存在;mv同样,-n表示不覆盖,-b覆盖前备份。- 别名陷阱:很多发行版给 root 配了
alias rm='rm -i',交互式很爽,脚本里rm卡在确认上。脚本开头unalias -a 2>/dev/null或直接写/bin/rm。 - 用
ls | xargs rm处理带空格的文件名会出大事,正确做法是find … -print0 | xargs -0 rm --(第六章、第八章)。
三、查看文件内容:cat、head、tail、less、file
看日志、看配置、看二进制里嵌的字符串——目的不同,工具差别很大。选错工具的后果通常是:cat 一个 8GB 日志把终端刷爆,或者用 less 打开后不知道怎么退出。
1. cat —— 一次性输出整个文件
cat 适合短文件;长文件请用 less,超大文件先看体积(ls -lh 或 wc -c)。
示例 1:直接查看
cat /etc/nginx/nginx.conf # 整个文件输出到终端
示例 2:带行号
cat -n app.py # -n 每行前面加行号(空行也编号)
nl file # 类似但默认只给非空行编号,nl -ba 则全部编号
示例 3:查看不可见字符(查 Windows 换行必备)
cat -A Makefile # 行尾显示 $、Tab 显示 ^I、CR 显示 ^M
看到 ^M$ 就说明文件是 CRLF 行尾,用 sed -i 's/\r$//' file 或 dos2unix file 处理。
示例 4:拼接文件
cat part_* > full.bin # 顺序按参数顺序(glob 展开按名字排序)
tac access.log # tac 按行倒序输出,看倒序日志很方便
2. head —— 只看开头
示例 1:前 N 行
head -n 200 big.csv # 前 200 行;-n 指定行数,默认 10 行
head -c 512 file # -c 按字节取
示例 2:去掉末尾几行
head -n -5 data.txt # 输出除最后 5 行以外的全部内容(GNU 扩展,BusyBox 不支持)
3. tail —— 只看末尾与实时跟踪
示例 1:最后 N 行
tail -n 50 app.log # 默认 10 行
示例 2:实时跟踪日志(最常用的排障动作)
tail -f -n 200 app.log # -f 跟踪追加内容,Ctrl-C 退出
tail -F /var/log/syslog # -F 在文件被 logrotate 轮转后会重新打开,生产环境用大写 F
-f 在日志轮转后会静默停止,这是「tail 看着没动静其实服务在写新文件」的常见原因。
示例 3:跟踪 + 过滤
tail -f app.log | grep --line-buffered ERROR
不加 --line-buffered 会因为管道缓冲看起来「卡住了」。看多文件时 tail -f 会在两个文件间轮流转屏,容易看漏;逐个终端各开一个,或用 multitail。
4. less —— 分页阅读
less 是看长文件的标准答案(名字来自「LESS is more」);more 只能往前翻,除了「机器上只装了它」之外没有理由选它。
示例 1:打开文件
less file # 打开后按 q 退出——新手最常见的卡住点
| 键 | 作用 | 键 | 作用 |
|---|---|---|---|
Space / b | 下/上一页 | g / g | 跳到第一行 |
Enter / y | 下/上一行 | G | 跳到最后一行 |
/关键词 | 向下搜索 | ?关键词 | 向上搜索 |
n / N | 下一个 / 上一个匹配 | &关键词 | 只显示匹配行(过滤视图) |
d / u | 半页下/上 | q | 退出 |
F | 进入跟踪模式(类似 tail -f) | v | 用 $EDITOR 打开当前文件 |
-N(启动参数) | 显示行号 | Ctrl-G | 查看当前位置信息 |
示例 2:常用启动写法
less -N file # 显示行号
less +G file # 打开即停在底部
less +F app.log # 打开即跟踪,像 tail -f,按 Ctrl-C 回到翻页
less -S file # 长行不折行,用左右箭头横向滚
5. file —— 判断文件的真实类型
示例 1:不看扩展名看文件头
file img.png app.so # 输出如:PNG image data, 800 x 600, 8-bit/color RGB
file 判断的是文件头(magic number),因此「扩展名 jpg 实际是 png」这种问题只有它能发现。
6. od、hexdump、xxd、strings —— 看二进制
示例 1:字节转储
od -c -N 64 file # 八进制/字符转储,\n、\0 直接可见;-N 64 只看前 64 字节
hexdump -C -n 128 file # 经典十六进制视图:左偏移、中 16 字节、右 ASCII
xxd file # 同上;xxd -r hex.txt bin 还能把 hex 文本反向写回文件
示例 2:抽出可打印字符串
strings -n 6 app.so # 默认最短 4 个字符,-n 6 减少噪音
strings -a /usr/bin/nginx | grep -E '^https?://' # 找嵌在二进制里的路径、报错文本、版本号
压缩包不用先解压也能看:zcat app.log.gz、zless app.log.gz(第九章详述)。
踩坑提醒
- 看完日志不记得怎么退出
less:按q。卡在任何分页程序里,第一反应都是q,不行再Ctrl-C。 cat大文件不会「只看前几行」,它会全部刷到终端,几 GB 的日志能让终端卡几十秒——先用wc -c或ls -lh确认体积。- 复制出来的配置「看着一样但跑不通」,九成是行尾
CRLF:cat -A看到^M$就用sed -i 's/\r$//' file或dos2unix file处理。
四、最小编辑器集:nano 与够用的 vim
服务器上手第一道坎往往不是命令而是编辑器:改一行配置,进了 vi 出不来,或者不知道怎么保存。这一章只讲两个编辑器——nano(新手,键位写在屏幕底部)和 vim(服务器上一定会遇到)——目标是「能改一行、能存盘、能退出」。
1. nano —— 键位就写在屏幕底部
nano file 打开后,屏幕底部的 ^ 代表 Ctrl,照着按就行。
示例 1:最常用的四个键
nano /etc/nginx/nginx.conf # 打开文件
# Ctrl-O 然后 Enter —— 保存(写入当前文件名)
# Ctrl-X —— 退出(有改动会问是否保存)
# Ctrl-W —— 搜索(Where Is)
示例 2:带参数打开
nano +42 /etc/nginx/nginx.conf # 打开即跳到 42 行
nano -l file # 保存时自动补末尾换行
想永久显示行号,在 ~/.nanorc 写 set linenumbers。完整按键表:
| 按键 | 作用 | 按键 | 作用 |
|---|---|---|---|
Ctrl-O 然后 Enter | 写入(保存)当前文件名 | Ctrl-X | 退出(有改动会问是否保存) |
Ctrl-W | 搜索(Where Is) | Alt-W | 重复上一次搜索 |
Ctrl-K / Ctrl-U | 剪切一行 / 粘贴回来 | Ctrl-^ | 替换(Replace) |
Ctrl-A / Ctrl-E | 行首 / 行尾 | Alt-A / Alt-E | 设标记起点 / 终点(选块) |
Ctrl-Shift-6(或 Alt-6) | 复制选中块 | Alt-Shift-3 | 删除选中块 |
Alt-/ | 撤销 | Alt-E(重复) | 重做(Ctrl-6 旧版) |
Ctrl-T | 打开拼写检查 | Ctrl-R | 把另一个文件插进来 |
2. vim —— 先分清三种模式
vim 的行为取决于当前模式,模式之间靠这几个键切换:
| 模式 | 进入 | 能干什么 | 屏幕底部特征 |
|---|---|---|---|
| 普通模式(Normal) | 启动即在此;Esc 回到 | 移动、删除、复制粘贴、: 命令 | 无提示 |
| 插入模式(Insert) | i a o | 打字 | -- INSERT -- |
| 命令行模式 | 普通模式按 : / ? | 存盘、退出、搜索、替换 | 光标跳到最底行 |
屏幕底部出现 -- INSERT -- 就说明你正在打字而不是在下命令——这是所有 vim 卡住的根源。
3. vim 够用的十二个键位
按「进得去、改得了、出得来」排序:
i # 在光标前进入插入模式(开始打字)
a # 在光标后插入
o # 在下方开一个新行并插入(加新配置行必备)
Esc # 回到普通模式——打完字的第一件事
:w # 保存(write)
:q # 退出(有未保存改动会拒绝)
:q! # 强制退出并丢弃改动——「怎么退出 vim」的正确答案
:wq # 保存并退出(也可以按 ZZ)
x # 普通模式下删除光标处一个字符
dd # 剪切一行(复制用 yy,粘贴用 p)
u # 撤销(重做 Ctrl-R)
/关键词 # 向下搜索,按 n 跳下一个(? 向上)
gg # 跳到文件第一行(G 跳最后一行)
4. 进阶但极常用的四个(改配置真的会用到)
:%s/old/new/g # 全文替换;加 c 变 :%s/old/new/gc 逐个确认
:10,20s/^/# / # 只在 10–20 行行首加注释(范围替换)
:30 # 跳到 30 行;:10,30d 删除 10–30 行
v # 可视模式(Ctrl-V 是列块模式,批量给多行加前缀)
5. 「怎么退出 vim」——这个梗的正确解法
网上「如何从 vim 退出」的笑话,真实成因是:用户按了 i 进入插入模式,改完直接敲 :q!,结果 : q ! 被当成普通文本写进了文件。解法固定三步:
- 连按
Esc两三次(保证回到普通模式); - 输入
:q!然后Enter(不想保留改动)或:wq(保留); - 如果底部显示
E37/E1734之类的报错,说明还有未保存内容或输入被当命令解析,先u撤销刚才误打的字符再:q!。
6. 只有 vi 时的应急改行
精简镜像里 vim 可能不存在(只有硬链接的 vi,且是 vi-compatible 模式),改一行配置有两种不动编辑器的办法:
sed -i.bak 's/^# *listen_port.*/listen_port 9080;/' /etc/app/app.conf
# -i.bak 原地修改并先留 .bak 备份;s 命令末尾的分号是给 nginx 的语法,别多别少
printf '%s\n' 'user deploy;' 'worker_processes auto;' > /etc/app/app.conf.new
# 小配置文件可以直接重建
改完先验语法再重载:nginx -t && systemctl reload nginx。真要用 vi 应急,键位与 vim 完全一致(i、Esc、:wq),只是缺多级撤销和语法高亮。
踩坑提醒
:w报E45: 'readonly' option is set:文件不可写。别退出去sudo vim重来,直接:w !sudo tee %就能以 root 写回当前缓冲。:q拒绝退出报E37,别急着:q!丢掉半小时改动,先:w!;确实不要了再:q!。- vim 保存后文件属主变成 root(用
sudo vim改的),导致应用写不了日志——要么chown回去,要么改用:w !sudo tee %。 - 编辑器里「粘贴的代码乱七八糟缩进」:vim 需要
:set paste(或新版:set pastetoggle=<F3>),否则会自动缩进把每行叠歪。
五、权限与属主:chmod、chown、sudo
Permission denied 是 Linux 里出现频率最高的错误,而它的成因至少有六种:文件权限、目录缺 x、属主不对、SELinux、capabilities、进程用的不是你以为的用户。这一章先把「读权限位」练熟,再给一套安全的修改顺序。
1. 先学会读权限位
| 字符位 | 对文件 | 对目录 |
|---|---|---|
r | 能读内容(cat) | 能列出名字(ls 目录本身) |
w | 能改内容(写同一文件) | 能在里面创建/删除文件(真正的删除权看目录,不看文件!) |
x | 能执行(配合 r 才能跑脚本) | 能进入(cd)并访问其中的已知文件名 |
关键结论:目录的 x 比文件本身的权限更决定性。chmod 644 app.sh 之后脚本不能跑是缺文件 x;而 ./app.sh 报 Permission denied 有时是它的父目录缺 x——没有 x 就无法解析路径,哪怕文件是 777。典型事故:家目录 chmod 700 ~ 之后,别人 cat /home/deploy/x.txt 必失败,即使 x.txt 是 644。
数字法:r=4、w=2、x=1,每组三位相加:
| 数字 | 含义 | 常见用途 |
|---|---|---|
644 | rw-r--r-- | 普通文件默认(配置、图片、代码) |
755 | rwxr-xr-x | 可执行文件、脚本、目录默认 |
600 | rw------- | 私钥、含密码的配置 |
700 | rwx------ | .ssh 目录、只给自己看的目录 |
664 | rw-rw-r-- | 团队协作目录里的文件(配合组权限) |
775 | rwxrwxr-x | 组内共享的目录 |
777 | rwxrwxrwx | 不要用,见下 |
2. chmod —— 修改权限
示例 1:数字法(整套设置)
chmod 644 app.conf # 属主可读写,属组和其他人只读
chmod 755 deploy.sh # 属主可执行,其他人可读可执行
示例 2:符号法(改单个位,安全且不牵连其他位,推荐)
chmod u+x deploy.sh # u=属主 +执行位
chmod g+w app.sh # g=属组 加写权限
chmod o-rwx private.conf # o=其他人 去掉全部权限;a=全部人
chmod -R a+rX /srv/app # 大写 X:只给目录或已是可执行文件加 x —— 修目录树的最佳写法
chmod -R a+rX /srv/app 是「让所有人能读,但只给目录和已有的可执行文件加执行位」的标准解法;写成 chmod -R 777 会把配置文件也变可执行,还会打开目录写权限。
示例 3:特殊位
chmod u+s helper # setuid:执行时以文件属主(root)身份,如 passwd
chmod g+s dir # setgid:目录里新建文件继承目录的组,团队共享目录用
chmod o+t dir # sticky:目录里只有属主能删自己文件,如 /tmp
chmod 777 的真实风险:任何本地用户都能改你的代码和脚本(配合 cron 或 web 目录可直接提权),多数服务(sshd、nginx)还会因为权限过宽拒绝启动。正确替代顺序:先 chown -R appuser:appgroup 找对属主,再 chmod -R a+rX(或 755/775)给最小权限。setuid 文件如果被写权限打开,等于本机任何用户能拿到 root,排查:find / -xdev -perm -4000 -type f 2>/dev/null(顺便看看有没有不认识的)。
3. chown 与 chgrp —— 修改属主属组
示例 1:改属主、改属组
chown deploy:app app.py # 同时改属主和属组;普通用户不能把文件送给别人,只有 root 能
chgrp -R www-data logs # 只改属组;比 chown 温和,不需要 root 也能改自己组的
chown -R app:app /srv/app # -R 递归整棵目录树
chown -h link newtarget # -h 改软链接本身,而不是它指向的文件
示例 2:大范围改动的安全网
chown -R --from=root app /srv/app # --from 只改当前属主是 root 的文件,避免误改新加的文件
find /srv/app ! -user deploy -print | head # 递归前先用它跑一遍确认范围
改属主时的三个雷:
chown -R在目录树里遇到别人正在写的数据库文件会打断服务——先停服务再改。- 递归前先跑一遍确认范围(上面的
find)。 /srv、/var/www这类由包管理器维护的目录,chown -R root:root之后可能连apt都跑不动。
4. umask —— 新文件的默认权限
umask 是「新建文件时要屏蔽掉的位」。文件最多 666,目录最多 777,然后按位与 ~umask。
| umask | 新文件 | 新目录 | 场景 |
|---|---|---|---|
022 | 644 | 755 | 默认(系统用户、多数发行版) |
002 | 664 | 775 | 团队协作组(配合共同属组) |
077 | 600 | 700 | 安全敏感环境 |
027 | 640 | 750 | 常见服务器加固基线 |
示例 1:查看与设置
umask # 查看,输出如 0002
umask -S # 符号形式(u=rwx,g=rx,o=),好读
umask 027 # 只对当前 Shell 及其子进程生效,写进 ~/.bashrc 才持久
坑:umask 只影响新建文件,不改动已存在的文件;用 mkdir 建了一半才发现 umask 不对,用 chmod -R a+rX 补。systemd 服务可以单独设 UMask=0027(在 unit 里),与登录 Shell 的 umask 无关——很多人卡在这里。
5. sudo 与 su —— 换用户的方式不同
| 维度 | sudo cmd | sudo -i / su - | su cmd |
|---|---|---|---|
| 需要谁的密码 | 通常自己的密码 | sudo -i 自己;su - 目标用户(root) | 目标用户 |
| 环境 | 保留大部分(secure_path) | 完整登录环境 | 不完整(su - 才完整) |
| 审计 | 每条命令写进 /var/log/auth.log 或 journal | 记会话 | 只记会话 |
| 可精细授权 | 能(只允许某些命令) | 等价于给整个 shell | 否 |
| 典型用途 | 跑一条特权命令 | 长期在 root 下操作 | 容器里 root 切普通用户 |
示例 1:常用写法
sudo !! # 上一条命令忘了加 sudo,重跑
sudo -u appuser ./worker.sh # 以某个服务用户身份跑(比给它 root 权限安全)
sudo -i # 拿到 root 的登录 shell(读 /root/.profile)
sudo -l # 列出我当前被允许做什么(排查「为什么这个 sudo 不行」)
sudo -v # 只刷新凭据缓存,测试密码对不对
sudo --preserve-env=PATH cmd # 需要保留自己的 PATH 时(默认 sudo 会重置)
示例 2:改 sudoers 的正确流程
/etc/sudoers 永远不要直接 vim 改——语法错了会把所有 sudo 权限锁死。
sudo visudo -c # 先检查现有语法
sudo visudo # 用 visudo 打开(编辑完保存前自动校验)
sudo visudo -f /etc/sudoers.d/deploy # 最好:单独放一个 drop-in 文件
drop-in 里写权限要最小化:
deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart nginx, /bin/systemctl status nginx
visudo 默认用 vi;想换 EDITOR=nano sudo -E visudo(-E 保留环境变量,否则换不了)。sudoers 目录里最后加载的文件生效,/etc/sudoers.d/ 里的覆盖主文件,命名别带 . 和 ~(会被忽略,README 那种文件写权限进去会炸)。
6. 特殊权限位一览
| 位 | 数字 | 表现 | 作用 | 常见例子 |
|---|---|---|---|---|
| setuid | 4000 | 属主执行位是 s(rwsr-xr-x) | 执行时以文件属主身份(root) | passwd、ping(旧式) |
| setgid | 2000 | 属组执行位是 s(rwxr-sr-x) | 文件:执行时以属组身份;目录:新建文件继承目录的组 | 团队共享目录 chmod g+s /srv/shared |
| sticky | 1000 | 其他人执行位是 t(drwxrwxrwt) | 目录里只有属主能删自己的文件 | /tmp |
7. setfacl / getfacl —— 「既要给第三个人读又不要给其他人」
普通权限只有三档,遇到「给某个组加读写、其他人不动」就需要 ACL:
getfacl /srv/app/data # 查看(含默认 ACL)
setfacl -m u:backup:rx /srv/app/data # -m 加一条:给用户 backup 读+执行
setfacl -m d:g:deploy:rwX /srv/shared # d: 默认 ACL,新建文件自动继承
setfacl -x u:backup /srv/app/data # -x 删一条
setfacl -R -b /srv/app # -b 递归清空所有 ACL
ls -l 末尾出现 +(如 -rw-rw-r--+)就说明有 ACL。坑:文件系统必须挂载带 acl 选项(现代 ext4/xfs 默认支持),NFS 上还要服务端配合;容器里 setfacl 常因缺 acl 包报错。
8. 「为什么 Permission denied」排查清单
| 检查顺序 | 命令 | 判断 |
|---|---|---|
| 1. 谁在执行这条操作 | ps -o user= -p $(pgrep -f worker) | nginx 是 www-data,你以为是 deploy |
| 2. 文件本身与所有父目录的权限 | namei -l /srv/app/logs/app.log | 一眼看全链路,父目录缺 x 立刻现形 |
| 3. 属主属组是否符合预期 | stat -c '%U %G %a' file | 部署脚本用 root 建的文件,进程写不了 |
| 4. 是否 SELinux/AppArmor 拦的 | sudo ausearch -m avc -ts recent;getenforce | 临时验证:sudo setenforce 0(测完必须 setenforce 1 恢复) |
5. 文件系统挂载是否 ro 或 noexec | findmnt -T /srv/app | 看到 ro 就是磁盘只读挂载(第十四章) |
| 6. 磁盘/inode 满(报的是别的错但很像权限) | df -h; df -i | No space left on device vs Read-only file system |
7. 目录缺 x、文件缺 r 的具体组合 | sudo -u appuser cat /srv/app/x | 用真实身份复现最快 |
| 8. 服务被沙箱限制 | systemctl cat myapp 看 ProtectSystem=、ReadWritePaths= | systemd 加固参数导致写不了 |
SELinux 一句话说明:getenforce 返回 Enforcing 时,即使权限位全对也可能被安全上下文拒绝,典型表现是 nginx 读得了 /srv/app 里 644 的文件却报 403。正确修法不是关掉 SELinux,而是 ls -Z 看上下文、semanage fcontext + restorecon 打标签。RHEL 系默认开,Debian/Ubuntu 默认没开。
踩坑提醒
- 改完组权限发现没生效:组成员变更不会作用于已登录的会话,重新登录或
newgrp 组名(第十二章)。 chmod -R 755把~/.ssh也改了,结果ssh立刻连不上:authorized_keys要 600、.ssh要 700。- 目录给了
w没给x(-wx)是极少见的组合:能创建/删除文件但不能列出内容,排查时容易误判,一般不这么配。 - 权限问题一律用
namei -l 全路径起手,比一层层ls -ld快且不会漏。
六、查找文件:find、locate、grep -r
「这个日志到底在哪」「谁把配置塞到系统里的」「按名字找命令」——三类需求对应 find、locate、type。find 参数多但有固定套路:先缩小范围(路径 + -maxdepth),再定条件(-name/-type/-mtime),最后才加动作(-print/-exec/-delete)。
1. find —— 按名字找
find 的基本格式是 find 起点 条件… 动作…,条件之间默认是「与」,! 取反,-o 或(注意 -o 要用括号分组)。
示例 1:按文件名查
find /srv/app -name '*.log' # -name 按 glob 匹配文件名;模式必须加引号,否则先被 Shell 展开就废了
find . -iname '*.conf' # -iname 忽略大小写
find /etc -path '/etc/nginx/*' # -path 匹配全路径(glob);-regex 是全路径正则(GNU)
示例 2:先缩小范围(好习惯)
find / -maxdepth 3 -name 'id_rsa' # -maxdepth 限定层数,要放在条件最前面;放后面会先全扫一遍再截
find / -xdev -name '*.env' # -xdev 不跨文件系统,避免扫进 /proc、/mnt、备份盘
find . -path ./.git -prune -o -name '*.js' -print # -prune 跳过子树,比 -not -path 快很多
示例 3:按类型 / 大小 / 时间 / 属主 / 权限
find /var -type l # -type:f 文件 d 目录 l 链接 s socket b/c 设备 p 管道
find . -size +100M # -size 按大小:+/- 号,c 字节、k KiB、M MiB
find logs -mtime +7 # -mtime 按修改天数:-7 七天以内,+7 超过七天,7 正好第七天(按 24h 计,不是日历天!)
find . -mmin -10 # -mmin 按分钟:最近 10 分钟改过,排查「刚刚谁在写文件」很好用
find /etc -newer /etc/hostname # -newer 比某个文件新,比 -mtime 精确;配合 touch -d 造时间锚点
find /home -user deploy # -user/-group 按属主;-uid 1001 用于用户已被删除的情况
find / -xdev -perm -4000 -type f # -perm:-4000 至少含这些位(找 setuid),644 完全等于
find . -type d -empty # -empty 空文件/空目录,清理部署留下的空目录
想精确到「最近 7 天」,最直观的是 -newermt "2026-09-27"(强烈推荐),不受 24 小时窗口影响。
2. find 的动作:-print、-exec、-delete
示例 1:先看范围,再动手
find build -type f -print # 每个结果一行
find . -name '*.tmp' -print0 # -print0 用 \0 分隔,交给 xargs -0(第八章),处理怪文件名安全
示例 2:对结果执行命令
find . -name '*.tmp' -exec rm -f {} \; # -exec … \; 每个文件 fork 一次;{} 替换为路径
find . -name '*.tmp' -exec chmod 644 {} + # -exec … + 把多个路径塞进一次调用,删/chmod 大量文件时快得多
find . -name '*.jpg' -exec convert {} {}.png \; # 文件少、命令重时用逐个执行
find . -name '*.tmp' -ok rm {} \; # -ok 每个都问一次,手生时兜底
示例 3:直接删
find logs -name '*.log' -mtime +7 -delete # -delete 内置删除,不 fork,隐含 -depth(先删文件后删目录)
-delete 与 -exec rm {} + 的差别值得记牢:
| 维度 | -delete | -exec rm {} + |
|---|---|---|
| 速度 | 快(无 fork,find 自己 unlink) | 每个批次 fork 一次 rm |
| 能否跑外部命令 | 不能 | 能(-exec sh -c '…' 任意组合) |
| 隐含行为 | 自动 -depth,先删文件后删目录 | 需自己保证顺序 |
| 对目录 | 只能删空目录(非空报 Directory not empty) | rm -r 能连子树一起删 |
| 误删风险 | 条件写错就当场删完 | 同样当场删,但可以换成 -print 先看 |
| 安全建议 | 永远先跑一遍去掉 -delete 的版本 | 同上;-exec rm -i 可加确认 |
危险的是 -delete 与 -o 组合时优先级误解:find . -name a -o -name b -delete 里 -delete 只作用于 -name b,要用括号 \( -name a -o -name b \) -delete。
示例 4:格式化输出做报表
find / -xdev -type f -size +200M -printf '%s\t%p\n' | sort -rn | head
# -printf 常用转义:%p 全路径、%f 文件名、%h 所在目录、%s 字节、%m 八进制权限、
# %u/%g 属主/组、%TY-%Tm-%Td 日期、%i inode 号(找硬链接)——这是「谁占了空间」的组合拳
3. locate —— 靠数据库,快但不实时
示例 1:秒查全盘
locate nginx.conf # 从数据库查,结果可能包含已删除的文件
locate -i 'id_rsa*' # -i 忽略大小写 + glob;默认按子串匹配,-r '^/etc/' 用正则
locate -b '\.conf' # -b 只按文件名基名匹配(不被当成路径一部分),少了误报
sudo updatedb # 刚建的文件查不到?手动重建数据库
数据库由 /etc/updatedb.conf 决定(PRUNEFS、PRUNEPATHS),NFS/FUSE/容器卷通常被排除,所以「locate 找不到但 find 能找到」多半是这个原因。没装时:Debian apt install mlocate、RHEL 系 dnf install mlocate(新版是 plocate)、Alpine 一般直接用 find。
4. grep -r —— 按内容找文件
示例 1:哪些文件里有某个词
grep -rl 'bind_port' /etc # -r 递归、-l 只列文件名
grep -rl --include='*.conf' 'listen' /etc # 限制文件类型
grep -rL 'strict' conf.d # -L 反向:输出没有匹配的文件名
grep -rn 'bind_port' /etc # -n 显示行号,直接看匹配在哪一行
grep -rlZ 'A' dir | xargs -0 grep -l 'B' # 找两个词都在的文件,注意 -Z/-0 配对
装了 ripgrep 的话 rg -l bind_port 默认尊重 .gitignore 且快一个数量级,开发机上推荐;服务器上不一定有,别写进部署脚本。
5. 查命令在哪:which / whereis / type
| 命令 | 会找到 | 例子 | 局限 |
|---|---|---|---|
which python3 | PATH 里的可执行文件 | /usr/bin/python3 | 看不到别名、函数、内建;不是 POSIX 标准 |
type -a python3 | 全部来源(别名/函数/内建/文件) | 三条一起列出 | 交互式排查首选 |
command -v python3 | 只输出路径(脚本标准写法) | /usr/bin/python3 | 内建会输出名字本身 |
whereis nginx | 二进制 + 源码 + man 页 | /usr/sbin/nginx /usr/share/man/man8/nginx.8.gz | 按固定目录猜,不看 PATH |
apropos backup | 帮助文本里含关键词的命令名 | rsync、dump… | 需要 man-db 索引存在 |
readlink -f $(command -v python3) | 剥开所有软链接到真实文件 | /usr/bin/python3.12 | 排查 alternatives 时常用 |
踩坑提醒
find的-name忘加引号:find / -name *.log会被当前目录里的*.log先展开,结果扫的是无关名字。-mtime -7与「最近 7 天」的直觉不完全一致(按 24 小时窗口而非日历天),要精确到分钟用-mmin -10080或-newermt "2026-09-27"(后者最直观,强烈推荐)。find输出直接喂给xargs会被空格/换行坑到,一律-print0配xargs -0。- 在
/上跑find会大量报Permission denied:加2>/dev/null静音,或用-xdev限制在当前文件系统。
七、文本处理三剑客:grep、sed、awk
日志分析、配置批改、数据统计,靠的就是 grep(找)、sed(改行)、awk(按列算)。再加五个小工具 sort、uniq、cut、tr、wc,能解决 95% 的「文本加工」需求。awk 与 sed 的分工一句话:改文本用 sed,算数据用 awk。
1. grep —— 查找匹配的行
grep 基本格式:grep [选项] 模式 文件…。
示例 1:基础查找
grep 'ERROR' app.log # 输出包含 ERROR 的行
grep -i error app.log # -i 忽略大小写
grep -n 'listen' nginx.conf # -n 显示行号
grep -c 'ERROR' app.log # -c 只输出匹配的行数
grep -w 'id' data.txt # -w 整词匹配,不会命中 width
grep -v '^#' app.conf # -v 反选(排除),常用它去注释
grep -F '1.2.3' version.txt # -F 按字面字符串匹配(不当正则),最快最安全
grep -m 1 'Start' big.log # -m 1 匹配到第一条就停,大文件里找开头很有用
示例 2:正则匹配
grep -E 'ERROR|FATAL' app.log # -E 扩展正则,或直接用 |(等价 egrep,但别再用 egrep)
grep -oE '[0-9]{1,3}(\.[0-9]{1,3}){3}' access.log # -o 只输出匹配到的部分——抠 IP
grep '\broot\b' pass.conf # \b 词边界(GNU)
grep '[[:space:]]$' conf # 找行尾空格([[:digit:]] 类写法不受 locale 影响)
正则元字符记忆表(ERE,即 -E 模式):
| 写法 | 含义 | 例子 |
|---|---|---|
. * + ? | 任意字符 / 0 或多 / 1 或多 / 0 或 1 | ^GET .* 200$ |
[0-9]、[[:digit:]] | 字符类(后者不受 locale 影响) | grep '[[:space:]]$' conf 找行尾空格 |
{n,m} | 出现次数区间 | grep -E '[0-9]{1,3}(\.[0-9]{1,3}){3}' 粗查 IP |
| | 或 | -E 'warn|error' |
^ $ | 行首 / 行尾 | grep -c '^$' 数空行 |
\b | 词边界(GNU) | grep '\broot\b' |
示例 3:上下文与多文件
grep -C 3 'Traceback' app.log # -C 3 上下各 3 行;-A 只有后、-B 只有前
grep -rl 'TODO' src # -l 只输出文件名
grep -q 'pattern' f && echo 有 # -q 不输出只给状态码,脚本判断「有没有搜到」专用
tail -f app.log | grep --line-buffered ERROR # 实时管道必备 --line-buffered
2. sed —— 按行替换与删除
sed 把输入按行读进「模式空间」,执行脚本后输出。核心是 s/找/换/标志。
示例 1:替换
sed 's/old/new/' file # 每行第一个匹配替换;只输出到终端,不改文件
sed 's/old/new/g' file # g 每行全部替换——不加 g 是常见遗漏
sed 's\|/a/b\|/a/c\|g' file # 换分隔符避开路径里的 /,也可用 #、@
sed 's/x/y/2' file # 只替换每行第 2 个匹配
sed -E 's/([0-9]+)/[\1]/g' # -E 扩展正则;() 分组、\1 回引用、& 代表整段匹配
注意 BRE(默认)与 ERE(-E)的区别:BRE 里 + ? {} 要写成 \+ \? \{,-E 后反过来不转义。
示例 2:取行与删行
sed -n '10,20p' file # -n 关掉默认输出 + p 只打印 10–20 行
sed '5d' file # d 删第 5 行;'5,9d' 删区间
sed '/^$/d' file # 删空行(匹配用 /正则/ 代替行号)
sed '/pattern/d' file # 删含 pattern 的行;想删它的下一行用 pattern/{n;d}
sed -n '/ERROR/,+3p' file # 匹配行及其后 3 行(GNU 扩展 +N)
示例 3:原地修改(先留备份!)
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.$(date +%F)
sed -i 's/^# *PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
diff /etc/ssh/sshd_config.$(date +%F) /etc/ssh/sshd_config
-i 表示 in-place 原地修改,写成 -i.bak 会自动留一份备份。差异提醒:BusyBox 不支持 -i 带后缀(写成 -i -e);macOS 必须给 -i ''。-i 会重建文件(新 inode),所以正在被 tail -F 跟的日志、以硬链接方式共享的文件、被进程按 inode 持有的文件都会受影响;nginx 之类的配置文件改完必须 reload 才生效。安全习惯:先不带 -i 跑一遍确认输出,再动手。
3. awk —— 按列算数据
awk 默认以空白分列,$1 是第一列、$0 是整行、NF 是列数、NR 是行号;结构是 '条件 {动作}'。
示例 1:取列
awk '{print $1, $9}' access.log # 打印第 1 列(客户端 IP)和第 9 列(状态码)
awk '{print $NF}' file # $NF 是最后一列(列数不定时必备)
awk -F'"' '{print $6}' access.log # -F 用 " 分列,第 6 段常是 URL
awk -F'\t' '$5=="WARN"' logs.tsv # 制表符分隔 + 精确匹配某列
示例 2:按条件过滤
awk '$9 == 500' access.log # 只输出状态码 500 的行
awk '$3 > 1000 {print $1}' stat.txt # 数值条件(字符串自动当数)
示例 3:聚合统计(awk 最经典的用法)
awk '{a[$1]++} END {for (k in a) print a[k], k}' access.log | sort -rn | head
# 按第 1 列计数、输出「次数 名字」再排序——Top N 统计的万能骨架
awk 'NR>1 {sum += $2} END {print sum/(NR-1)}' data.txt # 跳过表头求平均
awk -F, 'END {print NR}' data.csv # 数行数(比 wc -l 稳,能处理无末尾换行)
awk 'count[$7]++ > 0 {print $7} END {exit}' access.log # 找出第一个重复出现的 URL
awk 'BEGIN {OFS=","} {print $1,$2}' # OFS 控制输出分隔符
内置变量与函数速记:FS/OFS 输入输出分隔符、ORS 输出记录分隔符(设 "" 可拼接行)、FILENAME、FNR(当前文件的行号,多文件时与 NR 不同)、length()、split()、sub()/gsub()、sprintf()、toupper()、match() + RSTART/RLENGTH。
4. sort、uniq、cut、tr、wc、paste —— 五个过滤器
sort file # 默认字典序;受 locale 影响,脚本里加 LC_ALL=C 保证按字节稳定
sort -n # 按数字排(否则 10 排在 9 前面)
sort -h # 按人读大小排:2K 1G 10M 能正确排
sort -r # 降序
sort -V # 版本号感知:v1.10 排在 v1.9 后
sort -t: -k3,3n /etc/passwd # -t 指定分隔符;-k3,3n 按第 3 列数字排。注意 ,3 不能省——
# 只写 -k3 会从第 3 列一直排到行尾,结果和预期不符
sort -u # 去重(需已排序,等价 sort | uniq)
sort -m # 合并多个已排序文件
uniq -c file # 合并「相邻」重复行并计数——所以前面必须先 sort,否则看起来完全没去重
cut -d: -f1 /etc/passwd # -d 分隔符 + -f 取列;-f1-3 区间、--complement 取反列;只支持单字符分隔符
tr -d '\r' < win.txt # tr 逐字符替换/删除:删 CR、tr ' ' '\n' 一行变多行、tr a-z A-Z 转大写
wc -l file # 行数;-w 词数、-c 字节数;多文件会额外给 total 行
paste a.txt b.txt # 按列并文件;-d, 指定分隔、-s 转置行列
5. 三个日志统计的一行命令(照抄可用)
场景:/srv/app/logs/access.log,nginx combined 格式(IP - - [时间] "方法 路径 协议" 状态 字节 "referer" "ua")。
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10 # 1) 哪个 IP 请求最多(Top 10)
awk '{print $9}' access.log | grep -v '^2' | sort | uniq -c | sort -rn # 2) 状态码分布(只看非 2xx)
awk '{print $NF, $7}' access.log | sort -rn | head -20 # 3) 最慢的 20 个请求(假设 $NF 是耗时)
三条命令的结构都是「取列 → 归并计数 → 排序 → 截断」,学会一条就等于学会一片。想按小时聚合访问量,用 -o 抠出时间里的小时:
grep -oE '\[[^] ]+ [0-9]{2}' access.log | awk '{print $2}' | sort | uniq -c
踩坑提醒
grep在sh脚本里用grep -E,别再用废弃的egrep(新版会打 warning)。- 管道里最后一个命令的退出码才是
$?(第八章讲pipefail)。判断「有没有搜到」用grep -q:它不输出只给状态码,if grep -q 'pattern' f; then。 sed -i无备份直接改生产配置出事后无从回滚;养成sed -i.bak或先cp的习惯,改完diff一眼。awk里字符串比较要写$5 == "WARN"而不是$5 = "WARN"——一个等号是赋值,永远为真,这是 awk 最隐蔽的错误。uniq忘sort会得到「看起来完全没去重」的结果;反之sort -u会静默改变顺序语义(等价sort | uniq)。
八、管道、重定向与命令组合
Shell 的表达能力几乎全在这一章:把 stdout 接给下一个命令(|),把输出/错误写到别处(>、2>),把几条命令串成逻辑(&&、||)。搞不清这三个,脚本就只能写成猜谜。
1. 重定向 —— 三个流与 > 、>>、2>
每个进程有三个标准流:0 stdin、1 stdout、2 stderr。
cmd > out.txt # stdout 覆盖写文件
cmd >> out.txt # 追加——日志文件必备,> 会把历史冲掉
cmd 2> err.txt # stderr 单独写文件,只看错误
cmd > out.txt 2>&1 # stderr 并入同一个 stdout 再一起进文件。经典写法,顺序不能反!
cmd 2>&1 > out.txt # 反例:stderr 去了终端,stdout 才进文件(按从左到右解析)
cmd &> out.txt # 两者都进文件(bash 简写);POSIX sh 里没有,讲可移植时别用
cmd > /dev/null 2>&1 # 全部丢进黑洞,让 cron 安静运行
cmd < in.txt # stdin 来自文件
cmd 1>&2 # stdout 改道 stderr,让日志统一被 2> 捕获
>| 强制覆盖(即使开了 set -o noclobber);&> 与 >| 都是 bash 扩展。判断命令失败用 $?:0 成功、非 0 失败,具体数字看 man 的 EXIT STATUS 章节。
2. tee —— 既上屏又落盘
示例 1:边看边存
make install 2>&1 | tee -a /var/log/deploy.log # -a 追加;不加就是覆盖
示例 2:绕过「>> 需要提权」的问题
echo "10.0.0.5 internal" | sudo tee -a /etc/hosts
sudo some-cmd > /root/secret.txt 的重定向是当前 Shell(无特权)执行的,所以会报 Permission denied——正确写法就是用 | sudo tee file。tee /dev/stderr 可把内容转错误流。
3. xargs —— 把参数交给命令
find . -name '*.tmp' -print0 | xargs -0 rm -f --
# 标准拼接:-0 处理空格/换行(与 -print0 配对使用);-- 防文件名被当参数(-rf 之类)
seq 1 9 | xargs -n 3 cmd # -n 3 每次最多传 3 个参数
find . -name '*.jpg' -print0 | xargs -0 -P 8 -n 1 convert # -P 8 并发 8 个进程,适合下载/压缩
echo a b c | xargs -I{} sh -c 'tar czf {}.tgz {}' # -I{} 把参数放在指定位置,每行起一个 shell
find . -name '*.tmp' -print0 | xargs -0 -r rm -- # -r 空输入不执行(GNU 特性),避免 cmd 空跑
xargs --show-limits # 查 ARG_MAX;超限时报 Argument list too long
xargs 与 -exec … + 的取舍:两者都能批量;-exec 更直白、不用考虑管道,xargs 更灵活(能在中间做 grep/sort)。只要文件名可能带空格或换行,就必须用 -print0/xargs -0 这一对,或者 find -exec。
4. && 、|| 、; 与子 shell
nginx -t && systemctl reload nginx # && 前者成功才跑后者——改完配置先验证
systemctl is-active app || systemctl restart app # || 前者失败才跑后者
cd /tmp ; make # ; 无条件依次执行(脚本里危险,见踩坑)
long_task & # & 后台执行;关终端会收到 SIGHUP
( cd /tmp && make ) # 子 shell:里面的 cd 不影响当前目录
{ cmd1; cmd2; } > log # 分组但同 shell(变量能保留);{ 后有空格、要带 ;
echo $(date +%F) # 命令替换:$() 可嵌套,新代码一律用它
time make -j4 # 统计耗时(bash 内建的 time 更准,-p 给 POSIX 格式)
$() 与反引号功能基本相同,但反引号内部的反斜杠和嵌套容易出错,新代码一律 $()。
5. here doc / here string
cat > /tmp/run.sh <<'EOF' # here doc:把多行喂给命令的 stdin
#!/bin/bash
set -euo pipefail
cd /srv/app && make deploy
EOF
wc -c <<< "hello world" # here string:单行输入,等价 echo "abc" | wc -c
sudo tee /etc/profile.d/java.sh >/dev/null <<'EOF' # <<'EOF' 原样不展开变量
export JAVA_HOME=/usr/lib/jvm/java-17
export PATH="$JAVA_HOME/bin:$PATH"
EOF
<<EOF(不带引号)会对内容做变量展开,<<'EOF' 原样保留——写含 $HOME 的配置文件时用后者。<<-EOF 允许脚本里用 Tab 缩进(只剥 Tab,不剥空格)。here doc 结束的标记必须独占一行、顶格、无尾随空格,否则 Shell 会继续等输入(表现为卡在 >)。
6. 后台任务与脱离终端
long_task & # 直接后台;关掉终端就收到 SIGHUP 会死
nohup long_task > run.log 2>&1 & # 标准做法:nohup 忽略 SIGHUP
jobs -l # 看后台列表:[1] 28414 Running …
fg %1 # 把 1 号作业调回前台(bg %1 继续后台);%1 是作业号不是 PID
disown -h %1 # 已经跑着的任务从当前 Shell 解绑的补救
tmux new -d -s build 'make deploy' # 更好的方案:可重连,服务器场景首选
Ctrl-Z 是挂起(SIGTSTP)不是退出,进程还在占内存;忘记处理会留一堆 Stopped 进程,fg 回来或 kill %1 收掉。
踩坑提醒
- 「明明加了
2>&1还是没抓到错误」:顺序写反了。>与2>&1按从左到右解析,cmd 2>&1 > f里 stderr 指向的是终端的那个1。 rm -rf dir && echo done与rm -rf dir; echo done在脚本里的差别可能决定你是否覆盖了错误目录(第二章)。xargs不加-r时,空输入也会执行一次命令——find … | xargs rm在极端情况下等价于rm无参数(报错),但换成xargs chmod 777就会拿当前目录开刀。- 变量赋值不要留空格:
NAME=deploy正确,NAME = deploy会被当成「执行名为 NAME 的命令」。 - 在管道里
if/while read循环会跑在子 shell,循环里改的变量出来后消失(bash 4.2+ 可用lastpipe,或把管道换成重定向done < file)。
九、压缩与归档:tar、gzip、zip
.tar.gz、.zip、.xz 三种东西经常被混为一谈:tar 负责打包(把多个文件合成一个,不改内容),gzip/xz 负责压缩(单流算法)。tar 的 -z/-j/-J 只是顺手帮你调了一下压缩器。
1. tar 的字母到底管什么
| 字母 | 含义 | 单独用 |
|---|---|---|
c | create 创建归档 | 与 x 互斥 |
x | extract 解包 | — |
t | table of contents 列内容 | 不解包先看清单 |
r | repend 追加(不能压缩) | 少用 |
u | update 只追加更新的文件 | 类似增量备份 |
f | 归档file 名,必须紧跟文件名 | -f - 表示 stdin/stdout |
v | verbose 打印过程 | 看进度用 |
z | 走 gzip(.tar.gz/.tgz) | 速度快、体积一般 |
j | 走 bzip2(.tar.bz2) | 慢、体积小 |
J | 走 xz(.tar.xz) | 最慢、最小,发行版源码包常用 |
a | 按后缀自动选压缩器(--auto-compress) | GNU tar 1.15+ 创建时可用 |
2. tar 常用写法
tar cf app.tar /srv/app # 打包不压缩:c 创建、f 后跟归档名
tar czf app.tar.gz /srv/app # 打包 + gzip;验证用 tar tzf app.tar.gz
tar cJf app.tar.xz /srv/app # 打包 + xz:最小、最慢
tar caf app.tar.xz /srv/app # a 按后缀自动选压缩器,不容易打错
tar xzf app.tar.gz -C /srv/app # 解包到指定目录:x 解、-C 目标目录(目录需已存在,-C 不会自动建)
tar tzf app.tar.gz | head # 解包前先看清单——必须的一步!没有顶层目录的归档(tarbomb)
# 会把一堆文件直接散在当前目录;新版 tar 可用 --one-top-level 自动套一层
tar xzf app.tar.gz etc/nginx/nginx.conf # 只解一个文件,路径按 tar tzf 里看到的原样写
tar xzf app.tar.gz -C /tmp --wildcards 'etc/nginx/*' # 只取归档里的某个目录
tar czf app.tgz --exclude='./node_modules' --exclude='*.log' . # 排除若干:--exclude 必须在创建之前给出
tar czf app.tgz -X /srv/exclude.txt . # 排除清单来自文件,一行一个 glob
sudo tar xzf app.tar.gz -C / # 保留权限解包:不加 sudo 时属主会变执行者
tar czhf a.tgz dir # -h 把软链接指向的内容打进去(默认只存链接本身)
3. tar 与管道:目录到目录的流式搬运
tar cf - -C /srv/app . | tar xf - -C /srv/app-backup # 目录到目录流式复制,保留权限,不落地中间文件
tar czf - -C /srv app | ssh deploy@backup 'cat > /backup/app.tgz' # 跨机实时打包传过去
ssh deploy@server 'tar czf - /srv/app' | tar xzf - -C /local/backup # 反过来,从远端拉一个目录
tar cf - 里的 - 是 stdout,xf - 里的 - 是 stdin。这种写法的价值:不产生临时文件、能跨过空间不足的目标盘、管道里还能插一层 gzip。
4. gzip、bzip2、xz、zstd —— 单文件压缩器
gzip -9 app.log # 压缩并替换原文件:-1 最快、-9 最小(默认 6);-k 保留原文件
gunzip app.log.gz # 解压(等价 gzip -d);解压后 .gz 消失
bzip2 -9 file # 更高压缩、更慢,大文件慢到不可用
xz -9e file # 当前通用最优:-9e 额外努力;解压很快
zstd -19 file # 现代首选(快 + 小),服务器未必装,Alpine 仓库有
zcat app.log.gz # 不解压直接看
zgrep -h 'ERROR' /var/log/*.gz # 不解压直接搜——查历史日志必备,-h 去掉文件名前缀
zless app.log.gz # 分页看压缩包(less 配了 LESSOPEN 也能直接开 .gz)
zgrep 处理多个 .gz 时按文件名顺序扫,配合 ls -r 可以从最新往回找,找完立刻停。
5. zip / unzip
zip -r site.zip dist/ # 压缩目录:-r 递归;-9 最高压缩、-q 安静
zip -ru site.zip dist/ # -u 只加更新的文件(zip 支持增量更新归档)
zip -erP 'pass' site.zip dist/ # 加密码:传统 ZipCrypto 很弱,敏感数据别用 zip
unzip -l site.zip # -l 只列内容不解包
unzip site.zip -d /srv/app # -d 解压到目录;目录不存在会创建(与 tar -C 相反)
unzip -o site.zip # -o 全覆盖 / -n 不覆盖——默认会逐个问,脚本里必须指定
zipinfo -1 site.zip # 比 unzip -l 灵活:-h 头部、-s 大小列表
zip -FF broken.zip --out fixed.zip # 传输截断时抢救
tar 与 zip 的选择:Linux 服务器之间用 tar.gz/xz(保留权限、时间戳、软链接),要给 Windows 或走浏览器下载用 zip。zip 不保存 Unix 权限位,解压后脚本全部丢执行位。
踩坑提醒
tar忘记-f后跟文件名(如tar -czf app)会把-当成文件,或报Cowardly refusing to create an empty archive;f永远放参数最后。- 打包整个
/会卡在/proc、/sys上:加--exclude={/proc,/sys,/dev,/run},或直接用专门的备份工具。 - 压缩前先看体积:
du -sh;对一个正在被写入的日志压缩会得到两份都不完整的文件,先cp快照或用logrotate的copytruncate。 gzip -9与-6的体积差常在 1–3%,时间差却是几倍,别为了「更小」把部署脚本拖慢。.tar.gz想追加文件是不可能的(压缩流不能追加),需要重打包——这是很多人踩过的设计限制。
十、进程:ps、top、kill
「服务没起来」的第一现场永远是进程列表:它在不在、属主是谁、CPU 吃了多少、状态是 S 还是 Z。这一章把 ps 的每一列、top 的每个数字、信号的优先级讲清楚,然后给出杀进程的正确顺序。
1. ps —— 抓一份进程快照
ps 有两种参数风格,效果接近:
ps aux # BSD 风格:USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
ps -ef # POSIX/UNIX 风格:UID PID PPID C STIME TTY TIME CMD,带父进程号
ps -fp 28414 # 看单个 PID 的详情(含父进程)
ps -u deploy -o pid,cmd # 只看某个用户起了什么
ps aux --sort=-rss | head # 按内存排序,找吃内存的大户
ps -eo pid,ppid,user,%cpu,rss,etime,cmd --sort=-%cpu # 自定义列(最推荐,脚本里稳)
pstree -ap 28414 # 进程树看父子关系(-a 带命令行)
ps aux 各列含义:
| 列 | 含义 | 读法与坑 |
|---|---|---|
USER | 属主用户 | 服务真实用户常是 www-data/nginx,不是你 |
PID | 进程号 | 唯一,kill 用它 |
%CPU | 自进程启动以来的平均 CPU 占比 | 单核满载 = 100;8 核全用可到 800;多核机器上超 100 完全正常 |
%MEM | RSS 占物理内存百分比 | 与 RSS 一致,别拿它当「真实占用」 |
VSZ | 虚拟地址空间大小(KB) | 包含未实际分配的、映射的、共享的,JVM/数据库常见几个 G 而 RSS 很小 |
RSS | 常驻物理内存(KB) | 实际占用看这一列;共享库会被多个进程重复计 |
TTY | 控制终端 | ? 表示无终端(服务/后台进程) |
STAT | 状态码 | 见下表,排障关键 |
START | 启动时刻 | 显示日期表示不是今天起的 |
TIME | 累计 CPU 时间(不是墙上时间) | 跑了三天但 TIME 只有 5 分钟 = 一直在等 IO |
COMMAND | 命令行 | ps 只显示一次快照,top 会跟着改 |
STAT 常见组合:
| 码 | 含义 | 说明 |
|---|---|---|
R | 运行/可运行 | 在 CPU 上或在队列里 |
S | 可中断睡眠 | 绝大多数正常服务的状态 |
D | 不可中断睡眠 | 通常在等磁盘/NFS;kill -9 对它无效,这是「进程杀不掉」的头号原因 |
T | 停止 | 被 Ctrl-Z 或 SIGSTOP 挂起 |
Z | 僵尸(defunct) | 已退出但父进程没 wait;无法再被杀,只能让父进程回收 |
< / N | 高优先级 / 低优先级 | nice 值的标记 |
l / s / + | 多线程 / 会话首领 / 前台进程组 | s 常见于服务进程 |
2. pgrep 与 pidof —— 找 PID
pgrep -f 'gunicorn.*app' # -f 按正则匹配整条命令行;不加 -f 只看程序名(15 字符截断)
pgrep -u deploy -f worker # 叠加用户限定,更精确
pgrep -l -f nginx # -l 同时打印进程名,快速核对是不是自己要的
pidof nginx # 按可执行文件名精确列出所有 PID:28414 28415
pidstat -p 28414 1 # 持续采样某进程的 CPU/IO(sysstat 包),排「这个进程到底在忙什么」
用 pgrep -f 时的自杀风险:pkill -f 'tail -f app.log' 可能匹配到执行这条命令的 shell 本身。稳妥写法是加 -x(精确名)或用 pgrep 先列出确认。
3. top —— 实时视图
top 的头部五区域与关键读数:
| 区域 | 样例 | 怎么读 |
|---|---|---|
| 第 1 行 | top - 09:24:31 up 14 days, 3:12, 2 users, load average: 0.32, 0.41, 0.38 | 当前时间、运行时长、登录数、1/5/15 分钟平均负载 |
| 第 2 行 | Tasks: 183 total, 1 running, 182 sleeping, 0 stopped, 0 zombie | zombie 非 0 说明有父进程不回收 |
| 第 3 行 | %Cpu(s): 12.3 us, 3.1 sy, 0.0 ni, 83.9 id, 0.4 wa, 0.0 hi, 0.3 si | us 用户、sy 内核、id 空闲、wa 高等待说明磁盘瓶颈 |
| 第 4-5 行 | MiB Mem: 7861.4 total, 214.3 free, 3210.5 used, 4436.6 buff/cache / MiB Swap: 2048.0 total, 1900.0 free | 看可用内存要算 free + buff/cache(第十七章细讲) |
| 进程表 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND | VIRT 虚拟、RES 常驻、SHR 共享;真正吃的是 RES |
示例 1:常用启动方式
top -p 28414 # 只看某个进程
top -u deploy # 只看某个用户
top -b -n 1 > snap.txt # 批处理模式抓一次快照,脚本必备
示例 2:交互键(进入 top 之后按)
| 键 | 作用 | 键 | 作用 |
|---|---|---|---|
q | 退出 | 1 | 展开每个 CPU 核心 |
P / M | 按 CPU / 内存排序 | T(小写 t) | 切换头部显示 |
c | 显示完整命令行 | u / U | 只看某用户 |
f | 选择显示哪些列 | k | 对某 PID 发信号(会二次确认) |
r | renice 某 PID | G | 切换到某个 cpuset |
H | 显示线程 | g | 保存/切换视野组 |
| 空格 | 立即刷新 | o/R | 改变刷新间隔 / 反向排序 |
htop(apt install htop)好在:彩色树状视图、鼠标可点、F4 过滤、F9 送信号、F6 排序、Ctrl-L 重绘。%CPU 超 100 的解释:一个 8 线程的进程把 2 个核跑满会显示 200%,多核机器上 top 的 100% 指的是「一个核」,不是总机器的 1/8;判断是否真忙要看展开后的每核利用率(按 1)。
4. kill 与信号 —— 正确顺序(三步,别跳)
| 信号 | 编号 | 默认行为 | 能不能被捕获 | 用法 |
|---|---|---|---|---|
HUP | 1 | 挂断 / 让服务重读配置 | 能 | kill -HUP 28414(老式重载,nginx 仍支持) |
INT | 2 | 中断(等同 Ctrl-C) | 能 | 交互场景 |
QUIT | 3 | 退出并 dump core(Ctrl-\) | 能 | 有些服务用它输出栈 |
TERM | 15 | 请求终止(默认) | 能 | kill 28414 就是它 |
KILL | 9 | 立刻杀死,内核回收 | 不能 | 最后手段,见下 |
STOP | 19(x86 Linux) | 强制暂停,不可捕获 | 不能 | 冻结进程做调试;停止状态要 CONT 恢复 |
CONT | 18 | 恢复被 STOP 的进程 | — | kill -CONT 28414 |
USR1/USR2 | 10/12 | 自定义 | 能 | 各服务含义不同(看服务文档) |
kill 28414 # ① SIGTERM(15):给程序机会关连接、刷缓冲、清 pid 文件
sleep 10; ps -p 28414 # ② 等 10 秒确认,没退出再说
kill -9 28414 # ③ 仍不掉才 SIGKILL:内核硬拆,不做清理
kill -9 的代价:不执行信号处理函数 → 临时文件/锁文件/pid 文件残留、半写的数据库文件、子进程变孤儿、共享内存段没释放。很多「服务重启后卡在启动中」其实是 -9 留下的锁没清。对 D 状态进程 -9 也无效——它在等内核 IO,只能等 IO 完成或修好底层(磁盘/网络存储)。
其他常用发法
kill -15 28414 # 显式指定信号,等价 kill 28414
kill -TERM -28414 # 负号 = 整个进程组,清理派生一堆子进程的任务
kill %1 # 按当前 Shell 的作业号
kill -0 28414 # 不发送,只测进程在不在且有没有权限——脚本里的标准存活检查
killall nginx # 按名字(精确匹配,默认区分大小写);killall -u deploy vim
pkill -TERM -f 'gunicorn app' # 按命令行正则,先温和来一遍;确认杀不掉再上 -9
5. nice、renice、ionice —— 调优先级
nice -n 10 ./batch.sh # 以低优先级启动:-20(最高)到 19(最低),0 是默认
renice -n 10 -p 28414 # 运行中调优先级;调高需要 root;-u deploy 批量改某用户所有进程
ionice -c 3 -p 28414 # 调 IO 优先级(-c3 空闲级),备份任务必备,cfq/bfq 调度器下才有效
chrt --deadline 0 1000000 cmd # 实时调度类——一般业务别碰,会把机器拖死
6. 僵尸与孤儿
ps -eo pid,ppid,stat,cmd | awk '$3 ~ /Z/' # 找僵尸进程(<defunct> 的那些)
ps -eo pid,ppid,cmd | awk '$2 == 1' # 看被 init 收养的孤儿
vmstat 1 5 # 顺带一眼看全局
僵尸进程不占 CPU 和内存,只占一个 PID 表项,但会持续消耗父进程的资源并可能填满 PID。正确处理不是杀僵尸(杀不掉),而是:让父进程 wait(修代码里的信号/子进程回收)、或重启父进程;如果父进程是 init/systemd 且仍在回收,僵尸会自己消失。孤儿进程是被过继给 PID 1 的活着的孩子,本身无害。
vmstat 1 5 的四列:r(运行队列长度,持续大于核数说明 CPU 不够)、b(阻塞在 IO 的进程数)、si/so(swap 进出,非 0 说明内存紧张)、wa(IO 等待百分比)。这几列在第十七章排障时是主力。
踩坑提醒
ps aux的%CPU是平均值,抓瞬时值用top或pidstat 1;「ps 显示 3% 但机器很卡」通常是采样时机问题。VSZ大而RSS小不代表泄漏:JVM、mmap 大文件、malloc池都会虚高,判断泄漏看RSS的长期趋势。- 别对 systemd 托管的服务用
kill -9:systemd 会按Restart=立刻拉起来,看起来像「杀不死」。正确做法是systemctl stop(第十一章)。 pkill -f加sudo时容易误杀自己的 SSH 会话(匹配到了命令行里的字符串)。执行前pgrep同一条件先看结果。- 僵尸 + 高负载的组合,根因常常是磁盘
D状态进程堆积,先用ps -eo stat,cmd | grep '^D'确认。
十一、服务与定时任务:systemctl、journalctl、crontab
现代 Linux 的服务由 systemd 管,日志由 journald 收,定时任务由 cron 跑。这一章覆盖三件套,重点在 systemctl 的正确用法与 journalctl 的过滤参数——它们决定了你能不能在三分钟内定位「服务为什么起不来」。
1. systemctl —— 管理服务
systemctl status nginx # 看状态、最近日志、主 PID:绿=active、红=failed、黄=activating
systemctl start nginx # 启动;不改变开机是否自启
systemctl stop nginx # 停止:发 ExecStop(无则 SIGTERM),超时 90s 后 SIGKILL
systemctl restart nginx # 重启,有中断;能 reload 就别 restart
systemctl reload nginx # 重读配置不断服务;服务必须支持 ExecReload,否则报错
systemctl reload-or-restart nginx # 支持就 reload,否则 restart——部署脚本里的标准写法
systemctl enable nginx # 设开机自启:只建符号链接,不会启动;加 --now 顺便起
systemctl disable nginx # 取消自启:不会停正在运行的服务;加 --now 一起停
systemctl enable --now app # 部署时统一这么写
systemctl is-active app # 查是否运行(--quiet 只要状态码,可进 if)
systemctl is-enabled nginx # 查是否自启:enabled/disabled/static/masked
systemctl daemon-reload # 改了 unit 文件后必须执行!不 reload 则新配置不生效
systemctl list-units --type=service --state=failed # 只看失败的单元
systemctl list-unit-files --type=service # 列所有可 enable 的单元(看开机状态)
systemctl cat nginx # 打印 unit 文件内容(含 drop-in 合并结果)
systemctl edit nginx # 建 drop-in 覆盖(推荐,不动原文件),保存后自动 daemon-reload
systemctl mask nginx # 彻底禁死(连手动 start 都不行);unmask 恢复
systemctl list-dependencies nginx # 依赖树:「谁还没起来导致我起不来」
systemctl show nginx -p ExecMainStartTimestamp,MainPID # 查任意属性,脚本取 PID/启动时间
systemctl list-timers --all # 看 systemd 定时器(比 cron 更好管的一类任务)
restart 与 reload 的取舍:reload 让进程自己重读配置、连接不断(nginx、haproxy 支持),但不是所有服务都实现;配置改了却没生效,先 systemctl show app -p Environment,ExecStart 确认它拿到的到底是什么。
2. unit 文件的最小结构
放在 /etc/systemd/system/myapp.service:
[Unit]
Description=My App worker
After=network-online.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/srv/app
EnvironmentFile=/srv/app/env.list
ExecStart=/srv/app/venv/bin/gunicorn -c gunicorn.conf.py app:app
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=3
StandardOutput=journal
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
| 字段 | 作用 | 常踩的点 |
|---|---|---|
Type | simple(默认,ExecStart 即主进程)/forking(传统守护进程)/oneshot(跑完就退)/notify(自己报就绪) | 脚本型任务用 simple 会因为脚本秒退被判失败,改 oneshot 或 Type=exec |
ExecStart | 启动命令 | 必须绝对路径;不能用 shell 特性(管道、*),需要时 ExecStart=/bin/sh -c '…' |
After= / Wants= / Requires= | 顺序与依赖 | Requires 强依赖(对方失败我也失败),Wants 弱依赖;After 只管顺序不产生依赖,两者要搭配写 |
Restart= | no/on-failure/always | 配合 StartLimitBurst,重启风暴会被限流并进入 failed |
User= / Group= | 运行身份 | 改了要 chown 数据目录(第五章) |
Environment= / EnvironmentFile= | 环境变量 | 文件里不要写 export,一行 KEY=VALUE |
WorkingDirectory= | 工作目录 | 不写就是 /,相对路径全崩 |
ProtectSystem= / ReadWritePaths= | 沙箱加固 | 开了 ProtectSystem=strict 又没给 ReadWritePaths 就会「Permission denied」而权限位是对的 |
TimeoutStartSec/TimeoutStopSec | 超时 | 启动慢的服务要放宽,否则反复被杀 |
[Install] WantedBy= | enable 时挂到哪个 target | multi-user.target(服务器)/graphical.target |
改配置的三种途径优先级:systemctl edit 服务名(生成 /etc/systemd/system/服务名.d/override.conf)> 自己写同名 drop-in 目录 > 直接改 /lib/systemd/system/xxx.service(会被包升级覆盖)。改完必须 systemctl daemon-reload。
3. journalctl —— 查服务日志
journalctl -u nginx # 某个单元的全部日志
journalctl -u nginx -f # 实时跟踪(等价 tail -f,但含重启前的日志)
journalctl -u nginx --since "2026-10-04 09:00" --until "10:00" # 时间窗;也支持 "1 hour ago"
journalctl -p err -u nginx # 只看错误及以上(emerg/alert/crit/err/warning/notice/info/debug)
journalctl -b # 本次启动的全部日志
journalctl -b -1 # 上一次启动——查无故重启必备
journalctl -xe # 末尾几行 + 每条的解释(x 更多细节,e 跳到末尾)
journalctl -k -b # 内核日志(等价 dmesg,但可按时段过滤)
journalctl _PID=28414 # 按字段查(字段=值 直接写)
journalctl -f -u app -u worker # 多个单元一起跟
journalctl -o json-pretty -u app # 换输出格式(json、short-iso、cat)
journalctl --disk-usage # journald 占了多少磁盘
journalctl --vacuum-size=200M # 主动收缩日志(也可 --vacuum-time=7d)
journalctl --list-boots # 列出所有启动记录
日志为空的两种常见原因:服务把日志写到了自己的文件(unit 里 StandardOutput=file: 或代码里 logging.FileHandler),以及权限不够——普通用户查全量日志需要 systemd-journal 组(sudo usermod -aG systemd-journal deploy)。持久化默认可能关闭:/etc/systemd/journald.conf 里 Storage=persistent,然后 systemctl restart systemd-journald,否则 /var/log/journal 不存在、-b -1 查不到上次开机的事。
journald 上限的四个参数(同一文件):SystemMaxUse=500M(磁盘上限,最有用)、SystemMaxFileSize=50M、MaxRetentionSec=30day、ForwardToSyslog=yes(转给 rsyslog)。
4. crontab —— 定时任务
crontab -l # 看当前用户的任务
crontab -e # 编辑(改完自动生效,无需重启 cron)
sudo crontab -u deploy -e # 读写别人的(root 权限)
crontab -r # 删除全部任务 —— 危险,没有二次确认
字段顺序与示例:
分 时 日 月 周 命令
0 3 * * * find /srv/app/logs -name '*.log' -mtime +7 -delete
*/5 * * * * /srv/app/healthcheck.sh >> /var/log/hc.log 2>&1
30 4 * * 1-5 /usr/bin/pg_dump appdb | gzip > /backup/app.sql.gz
@reboot /srv/app/startup.sh
| 写法 | 含义 | 易混点 |
|---|---|---|
* * * * * | 每分钟 | 五段依次是「分 时 日 月 周」,第 3 位是「几号」而不是小时,写反就永不执行 |
0 3 * * * | 每天 03:00 | 「日」和「周」同时限制时是或关系(cron 特性,不是 bug) |
*/5 | 每 5 分钟 | 分钟从 0 起算,*/7 不是严格每 7 分钟 |
1-5(周) | 周一到周五 | 0 和 7 都是周日;有的实现只认 0 |
23:59 式 | 不存在,必须 59 23 * * * | 顺序写反就是「59 分 23 时」→ 永远不跑 |
三个经典坑:
- PATH 不同。cron 的环境只有
/usr/bin:/bin,没有你的~/.bashrc里的路径。写绝对路径,或在 crontab 顶部加PATH=/usr/local/bin:/usr/bin:/bin和MAILTO=""。 %是特殊字符(被当换行/参数分隔)。crontab 里date +%F必须写成date +\%F,或者干脆把命令放进脚本文件(强烈推荐)。- 注释与输出。
#行会被忽略;任务有输出时 cron 会试图用sendmail发信,没装 MTA 就表现为「静默失败」。所有行末尾加>> /var/log/cron.out 2>&1或MAILTO=""。
排查有没有跑:grep CRON /var/log/syslog(Debian)或 journalctl -u cron(RHEL 系与 systemd 环境)。at 一次性任务的用法:echo '/srv/app/report.sh' | at 02:00 tomorrow,atq 看队列、atrm 3 取消;需要装 at 且 atd 在跑。
5. timedatectl —— 时间与时区
timedatectl # 一站式看时间、时区、NTP 状态:排「日志时间对不上」先看这里
timedatectl set-timezone Asia/Shanghai # 设时区;改完服务不一定立刻感知,Java 应用还要 -Duser.timezone
timedatectl set-ntp true # 开启 systemd-timesyncd 同步;NTPSynchronized=yes 才算同步上
timedatectl show-timesync --all # 看用的哪台 NTP 服务器;内网要配 /etc/systemd/timesyncd.conf 的 Server=
timedatectl set-time "2026-10-04 09:30:00" # 手动设时间:必须先关 NTP!时间回拨会让证书校验、
# cron、数据库主从全部乱套
chronyc sources # 用 chrony 时的对时源(RHEL 系默认 chrony,
ntpq -p # timedatectl 显示 inactive 不代表没同步)
hwclock -r # 读硬件时钟——双系统时间错乱(UTC vs 本地)就在这里
容器里的时间问题:容器共享宿主内核时钟,改容器时区只影响 TZ 环境变量(TZ=Asia/Shanghai),不要试图在容器里 set-time。
踩坑提醒
- 「改了 unit 没生效」= 忘了
systemctl daemon-reload;「改了 drop-in 还在用旧值」=systemctl cat看合并后的真实结果,systemd 的覆盖语义是空赋值(Environment=空值 = 清空)。 enable≠ 启动、disable≠ 停止。部署脚本里统一systemctl enable --now app。- crontab 里
@reboot执行时网络可能还没就绪,依赖外部服务要重试或用 systemd 的After=network-online.target。 - 日志时间戳乱掉的第二原因:应用自己设了 UTC 而系统是本时区(nginx access log 用本地、
journalctl用本地但导出的 JSON 是 UTC)。核对一遍date、timedatectl、应用配置三处。
十二、用户与组:id、useradd、usermod、passwd
权限问题的一半其实是身份问题:服务用的是哪个用户、这个用户在不在组里、改了组为什么没生效。这一章把用户/组的查询与增删命令,以及 /etc/passwd、/etc/shadow 的字段逐个讲清。
1. id、who、w —— 先看清现状
id # 当前用户的 uid/gid 与全部附属组:uid=1000(deploy) gid=1000(deploy) groups=1000(deploy),27(sudo)
id deploy # 查别人(按名或按号 id 1001)——排查服务用户最常用的第一步
whoami # 只有用户名;注意 sudo whoami 会显示 root——判断当前是不是特权态
w # 谁在线 + 在跑什么(含 LOGIN@ JCPU TTY FROM IDLE WHAT),比 who 信息多
who # 谁登录了:deploy pts/0 2026-10-04 08:55 (10.0.0.12)
last deploy # 历史登录(读 /var/log/wtmp),只看某人;lastb 看失败尝试(要 root)
getent passwd # 查所有账户(含 LDAP/SSSD/NIS)——脚本里用这个而不是读文件
getent group sudo # 看某个组的成员:判断「他到底在不在组里」
groups deploy # 只列组名,比 id 好读
users # 一行列出在线用户名
2. useradd 与 adduser —— 增删改用户
先说清一件事:useradd 与 adduser 不是一回事。
| 命令 | 性质 | 行为 | 用在哪 |
|---|---|---|---|
useradd | 底层二进制(各发行版都有) | 默认不建家目录、不设密码、不交互式 | 脚本、Dockerfile |
adduser | Perl 友好脚本(Debian/Ubuntu 独有) | 交互式问全名/密码,自动建家目录、拷 /etc/skel | 手工建号 |
sudo useradd -m -s /bin/bash -G deploy,app deploy2
# -m 建家目录、-s 给登录 shell、-G 附属组——RHEL 系手工建号只能这么写
sudo adduser deploy # Debian/Ubuntu 交互式建号
sudo useradd -u 1500 -e 2026-12-31 -c "CI runner" ci
# -u 指定 uid(容器/集群对齐必备)、-g 主组、-d 家目录路径、-e 账户过期日、-c 备注字段
sudo userdel -r deploy # 删用户并删家目录;不加 -r 会留下 /home/deploy(可能含密钥);
# 删前先 ps -u deploy 确认没进程
3. usermod —— 加组(本章最贵的一课在这)
sudo usermod -aG docker deploy # 加附属组:-a 是「追加」,必须写!
sudo usermod -aG docker,app deploy # 一次加多个组
sudo usermod -L deploy # 锁定账户(shadow 密码字段前加 !);-U 解锁
sudo usermod -l newname oldname # 改名;家目录要另外 mv 并 chown
sudo usermod -s /usr/sbin/nologin www # 禁登录 shell——服务账户的标准做法
sudo groupadd app # 建组;groupdel 删组
gpasswd -a deploy app # 老写法加组成员(等价 usermod -aG);-d 移除
groupmems -g app -l # 列组成员;-a/-d/-p 加/删/清空
newgrp app # 当前会话临时切换有效组——不重新登录也能生效的补救,exit 退出
usermod -aG 的 -a 为什么这么重要:usermod -G docker deploy(少了 -a)会把 deploy 的所有附属组替换成只有 docker——原本在 sudo 组里的用户当场失去 sudo 权限,若他正是唯一的管理员就只能靠 root 密码或单用户模式救。安全写法永远带上 -a。
改完组后该用户必须重新登录(或 newgrp)才生效,因为组成员身份在登录时被写进凭证,已有会话不会更新——这是「加了组还是 permission denied」的头号原因。
4. passwd 与 chage —— 管密码
passwd deploy # 改自己或指定用户密码(指定他人需 root)
passwd -l deploy # 锁定(密码前加 !);passwd -u 解锁
passwd -e deploy # 让下次登录必须改密码
passwd -d deploy # 清空密码(配合 SSH 密钥场景,谨慎)
passwd -S deploy # 看状态:P 可用 / L 锁定 / NP 无密码 + 最近改密日期
chage -l deploy # 看过期详情;chage -M 90 -W 7 -I 14 设策略
5. /etc/passwd 与 /etc/shadow 的字段
/etc/passwd(getent passwd deploy 的原始内容,7 个字段用 : 分隔):
deploy:x:1000:1000:Deploy User:/home/deploy:/bin/bash
| 位置 | 值 | 含义 | 备注 |
|---|---|---|---|
| 1 | deploy | 用户名 | 登录与 chown 用 |
| 2 | x | 密码占位符 | 真正的哈希在 shadow;这里是空表示无密码 |
| 3 | 1000 | UID | 0=root,系统账户 < 1000,65534=nobody |
| 4 | 1000 | GID(主组号) | 指向 /etc/group |
| 5 | Deploy User | GECOS 备注 | 全名/电话,finger 读这里 |
| 6 | /home/deploy | 家目录 | ~ 的来源;不存在则登录报错 |
| 7 | /bin/bash | 登录 shell | /usr/sbin/nologin、/bin/false = 禁交互登录 |
/etc/shadow(8 个字段,只有 root 与 shadow 组可读,权限 640):
deploy:$y$j9T$abc…:20550:0:90:7:14::
| 位置 | 值 | 含义 |
|---|---|---|
| 1 | deploy | 用户名 |
| 2 | $y$j9T$… | 密码哈希;前缀 $y$=yescrypt、$6$=SHA-512、$2b$=bcrypt;! 或 * 开头 = 锁定;空 = 无密码可登录 |
| 3 | 20550 | 最后一次改密距 1970-01-01 的天数(date -d "1970-01-01 +20550 days" 换算) |
| 4 | 0 | 改密后至少隔多少天才允许再改(防来回改历史密码) |
| 5 | 90 | 密码有效期天数(99999 = 永不过期) |
| 6 | 7 | 过期前多少天开始警告 |
| 7 | 14 | 过期后多少天宽限期,之后账户锁定 |
| 8 | — | 账户本身失效的天数(与密码无关) |
/etc/group 的 4 个字段:组名、x 占位(组密码基本不用)、GID、成员列表(逗号分隔)。注意:这里列出的成员只是「附加组」,用户在 /etc/passwd 里的 GID 主组不会出现在这里,所以「getent group deploy 里没有他」不代表他不属于 deploy 组。/etc/gshadow 存组管理员与组密码,字段与 shadow 类似。
相关命令:getent passwd 1000(按 uid 反查)、lastlog(每人最后登录,读 /var/log/lastlog 稀疏文件)、faillock --user deploy(pam_faillock 的失败计数,RHEL 系)。
踩坑提醒
- 新建用户忘了
-m,~不存在导致后续所有「写家目录」的脚本失败:useradd -m或在/etc/default/useradd里设CREATE_HOME=yes。 - 服务账户(
nginx、redis)默认 shell 是/usr/sbin/nologin,su - nginx必然失败;正确姿势sudo -u nginx /bin/bash -l(root 可以覆盖 shell)。 - 直接编辑
/etc/passwd用普通vim有并发风险(同时useradd会覆盖),要改就vipw/vigr(带锁与语法检查)。 - UID 不一致是集群环境的隐形故障源:NFS 共享目录 + 各节点 UID 不同 → 权限完全乱。跨节点统一
useradd -u 1500。
十三、网络:ip、ss、dig、curl、ssh
网络排障的顺序基本固定:本机有没有 IP → 路由到不到 → DNS 解析对不对 → 端口有没有监听 → 防火墙让不让过 → 应用返回什么。ip、ss、dig、curl 各管一段,本章按这个顺序给命令。
1. ip —— 取代 ifconfig 与 netstat
net-tools(ifconfig、netstat、route)在新发行版里默认不装了,iproute2(ip、ss)才是当前的标准,且能显示 ifconfig 拿不到的信息(每个地址的前缀长度、路由表来源、队列规则、命名空间)。
ip a # 看所有接口与地址(addr 的缩写)
ip -br a # 一行一接口,最清爽
ip -4 addr show eth0 # 只看 IPv4、指定网卡
ip r # 看路由表(route 的缩写)
ip route get 8.8.8.8 # 问「去往这个地址走哪条路、用哪个网卡」——排障神器
ip l # 看链路状态;ip link set dev eth0 down 改状态(要 root)
ip neigh # ARP 表(= arp -a),排查同网段不通
ip -s link # 接口统计:RX errors 非 0 说明网卡/驱动问题
ip route add default via 10.0.0.1 # 加默认网关——重启失效!永久配置要写 netplan/NetworkManager
ip netns # 网络命名空间列表;ip netns exec foo ss -tulpn
ip a 输出里要会看的三个东西:inet 10.0.0.12/24 brd … 是 IPv4 与掩码(/24 = 255.255.255.0)、state UP 或 NO-CARRIER 是链路是否插了线、scope global/link(fe80:: 是链路本地地址,不能直接当目标)。
2. ss —— 谁在监听哪个端口
ss -tulnp # 日常万能式:TCP + UDP、监听中的、带进程名
ss -tuln # 同上但无需 root(不显示进程列)
ss -tan state established # 只看已建立的 TCP 连接
ss -tp state established '( dport = :443 )' # 按端口过滤,能匹配到进程(netstat 做不到)
ss -s # 各状态连接数汇总(一眼看 TIME_WAIT 堆积)
ss -i # 拥塞控制、RTT 等 TCP 内部信息
ss -m # 内存占用(内核缓冲)
sudo ss -K dst 10.0.0.5 # 直接杀掉匹配的连接(需 root,4.9+ 内核)
字母含义:-t TCP、-u UDP、-l listening、-p 显示进程(需 root 才能看到别人的)、-n 数字不做名称解析(更快,避免 DNS 卡住)、-a 全部(含未监听)、-x Unix 套接字、-w raw、-s 统计、-i 内部信息、-e 扩展(uid/inode)。
与 netstat -antp 的对照:
| 需求 | netstat(老) | ss(新) | ss 的优势 |
|---|---|---|---|
| 列监听端口 + 进程 | netstat -tulnp | ss -tulnp | ss 直接从内核接口读,海量连接时快几十倍 |
| 全部 TCP 连接 | netstat -antp | ss -tanp | 支持 state 精确过滤 |
| 只看 TIME_WAIT | netstat -ant | grep TIME_WAIT | ss -tan state time-wait | 不用 grep,不丢格式 |
| Unix 域套接字 | netstat -xlp | ss -xlp | 输出更完整 |
| 路由表 | route -n | ip r | 支持策略路由显示 |
| 接口统计 | netstat -i | ip -s link | 含丢包/错误细分 |
ss 里 Recv-Q/Send-Q 在监听端口上的含义与连接上不同:监听行上它们是已完成队列长度 / backlog 上限,Recv-Q 接近 Send-Q 说明应用 accept 不过来(连接被丢弃),是「服务偶发 502/超时」的关键线索。
3. 连通性诊断:ping、dig、nc
ping -c 4 10.0.0.5 # 测 ICMP 可达与 RTT:-c 4 发 4 个包,-W 2 超时,-i 0.2 快速发
traceroute -n -w 2 host # 逐跳路径:-n 不解析域名(否则慢到不能用)、-T -p 443 用 TCP 探测
tracepath 8.8.8.8 # 无需 root 的替代,同时给出 PMTU
mtr -rwnc 50 host # traceroute + ping 合体:-r 报告模式、-w 宽、-c 50 发 50 包
dig +short app.example.com # DNS 查询首选:+short 只留答案、+trace 从根查起、+noall +answer 精简
dig A host @1.1.1.1 # 指定 DNS 服务器对比
nslookup app.example.com 8.8.8.8 # 简易 DNS(老工具):输出信息少,脚本别用
host -t MX example.com # 一行查 DNS;host -l example.com 测区域传送
getent hosts app.internal # 走本机完整解析链(含 /etc/hosts、nsswitch)——排「dig 能解析但 curl 不行」
nc -vz 10.0.0.5 5432 # 测端口通不通:-z 只探端口、-v 给结果(telnet 的替代,多数机器没装 telnet 了)
nc -l 8080 # 起临时 HTTP 靶机;nc -l 8080 > dump.bin 收文件(OpenBSD/GNU 版参数不同)
ethtool -i eth0 # 网卡驱动;ethtool -s 看速率/双工/链路——物理层问题唯一入口
注意两点:很多机房禁 ICMP,ping 不通不代表服务挂;mtr 中间跳丢包常是路由器限速的假象,只有最后一跳持续丢才是真问题。
4. curl —— 发请求与读 HTTP
curl -I https://app.example.com # 只看响应头(HEAD),验证 301/403/缓存头
curl -sSL -o page.html https://… # 下载标准式:-s 静默、-S 但报错仍显示、-L 跟随重定向、-o 存文件
curl -s -w '%{http_code}\n' -o /dev/null http://127.0.0.1:8080/healthz
# 健康检查一行式:只要状态码
curl -X POST -H 'Content-Type: application/json' -d '{"a":1}' URL # 发 JSON;-d @body.json 从文件读
curl -X POST --data-urlencode 'q=中文 词' URL # 表单编码(-d 不做编码,中文必错)
curl -u user:pass URL # Basic 认证;--netrc 用 ~/.netrc
curl --retry 3 --retry-delay 2 --retry-connrefused -sS URL # 重试,部署脚本必备
curl --connect-timeout 3 --max-time 10 URL # 超时控制,否则默认可以挂很久
curl -k URL # 跳过证书校验——只用于临时验证,别写进脚本;正确做法 --cacert ca.pem
curl -x http://10.0.0.9:8888 URL # 走代理
curl -v URL 2>&1 | head -40 # 看完整握手(TLS 版本、证书、请求/响应头)
curl --resolve app.example.com:443:10.0.0.12 https://app.example.com
# 本地强行指定 IP:验证负载均衡后面的某台机器
curl -T file URL --user u:p # 上传(PUT)
curl -s 'URL?page=1' | jq .total # 配合 jq 取字段
看耗时分布的一行格式串(可直接抄):
curl -s -o /dev/null -w 'code=%{http_code} dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total} size=%{size_download}\n' https://app.example.com
curl 的退出码有用:7 连不上、6 DNS 解析失败、28 超时、35 TLS 握手错、60 证书问题——脚本里判 $? 比看输出可靠。wget 的差异:递归下载(wget -r -np -nH -k URL)与自动断点续传(-c)更方便;单发请求与调试头信息一律 curl。aria2c -x 16 -s 16 URL 多连接下载大文件快很多,aria2c --input-file=list.txt 批量。
5. ssh、scp、rsync
ssh deploy@10.0.0.12 # 登录,默认 22;ssh -v 看握手细节(排「为什么拒绝」)
ssh -p 2222 deploy@host # 指定端口;配置里 Port 改了必须用 -p
ssh -i ~/.ssh/id_ed25519_prod deploy@host # 指定私钥;权限必须 600,否则报 UNPROTECTED PRIVATE KEY FILE
ssh -J bastion app # 跳板机(ProxyJump);多级 -J bastion1,bastion2
ssh -L 5432:db.internal:5432 -fN deploy@jump
# 本地端口转发:把远端能到的内网服务映射到本机 5432,
# 之后 psql -h 127.0.0.1 -p 5432 直连;-N 不开 shell、-f 后台
ssh -R 8080:localhost:8080 deploy@jump # 远程端口转发:把本机服务暴露到跳板机(需 GatewayPorts=yes)
ssh -D 1080 deploy@jump # SOCKS 代理:curl --socks5 127.0.0.1:1080 URL 配合用
ssh -o ServerAliveInterval=30 host # 防中间设备断连;写进 ~/.ssh/config 更省事
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@host # 一键装公钥,仍需一次密码,之后免密
scp app.tar.gz deploy@host:/srv/app/ # 复制:注意 scp 语义像 cp;传目录要 -r;新版走 SFTP 协议
rsync -avz --delete src/ host:/srv/app/ # 增量同步/备份——见下
rsync -a --progress --partial big.iso host:/data/ # 断点续传大文件
~/.ssh/config 是可维护性的关键(文件权限必须 600),一份够用多年的样例:
Host *
ServerAliveInterval 30
AddKeysToAgent yes
IdentityFile ~/.ssh/id_ed25519
Host prod-web
HostName 10.0.0.12
User deploy
IdentityFile ~/.ssh/id_ed25519_prod
ProxyJump bastion
Host db-tunnel
HostName bastion
User ops
LocalForward 5432 db.internal:5432
之后 ssh prod-web 一条搞定跳板与密钥。Host * 段的顺序只在第一次匹配时生效,所以具体 Host 要写在通配前面。
rsync 为什么更适合备份:只传差异(滚动校验)、可保留权限与时间戳(-a = -rlptgoD)、能删除目标多余文件(--delete 镜像语义)、可断点续传、可通过 ssh 加密传输、支持 --exclude 与 --link-dest 做硬链接式全量快照(省空间又每个快照都是完整目录)。
rsync -avz --delete --exclude='node_modules/' --exclude='.git/' /srv/app/ deploy@backup:/backup/app-$(date +%F)/
rsync -a --delete --link-dest=../app-2026-10-03 /srv/app/ /backup/app-2026-10-04/
--delete 务必先干跑:加 -n --delete(试运行)或 -i(显示每一项动作)。rsync 的路径语义坑:源 src/(带斜杠)= 同步目录内容,源 src(无斜杠)= 在目标下建 src 目录,这一条能造成整站目录结构错位。
6. 防火墙与 /etc/hosts
| 后端 | 常见于 | 常用命令 |
|---|---|---|
firewalld | RHEL/Rocky/Fedora | sudo firewall-cmd --permanent --add-port=80/tcp → sudo firewall-cmd --reload;sudo firewall-cmd --list-all |
ufw | Ubuntu/Debian 桌面与轻量服务器 | sudo ufw allow 22/tcp、sudo ufw deny 3306、sudo ufw status verbose、sudo ufw enable(开之前务必先 allow ssh) |
iptables / nftables | 手工管理或老环境 | sudo iptables -L -n -v --line-numbers、sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT;sudo nft list ruleset |
三条通用提醒:iptables -A 是追加到链尾(默认策略 DROP 时可能永远走不到),插入用 -I INPUT 1;规则改完不保存重启就没了(iptables-save > /etc/iptables/rules.v4,或 netfilter-persistent save);ufw/firewalld 都在最上层,用底层命令加的规则它们看不见,别混用两套。Docker 会直接写 iptables 并在启动时插入自己的链,ufw deny 拦不住已 expose 的容器端口——这是极常见的困惑来源。
/etc/hosts 是本机静态解析,优先级在 nsswitch 里通常高于 DNS(/etc/nsswitch.conf 的 hosts: files dns):
printf '10.0.0.12 app.internal\n' | sudo tee -a /etc/hosts
用途:临时指向某台后端验证、屏蔽域名(指 127.0.0.1)、容器里注入别名。坑:改了 hosts 但应用没生效,通常是应用做了自己的 DNS 缓存,或它走的是 dig 式直接查询;反之 hosts 会盖掉 DNS,测试完记得删。
踩坑提醒
- 「
ss显示监听在127.0.0.1:8080,外部怎么都连不上」——服务只绑了回环地址,改0.0.0.0:8080(或配置里的bind)。这一条比防火墙更常是根因。 ping不通不代表服务不可用(ICMP 被封),要用nc -vz或curl测目标端口才算数。- 排 DNS 问题按链路顺序:
getent hosts X(含 hosts 文件)→dig +short X @<本机 resolv 里的服务器>→ 换@1.1.1.1对比 → 查/etc/resolv.conf(systemd-resolved环境里它是指向127.0.0.53的软链接,直接改会被覆盖,正确改法是/etc/systemd/resolved.conf的DNS=)。 - 容器里
/etc/hosts与resolv.conf是挂载文件,chmod/chown会报Device or resource busy。 mtr中间跳的丢包率是路由器 ICMP 限速造成的假象,只看最后一跳与整体 RTT。
十四、磁盘:df、du、mount、fsck
No space left on device 有两种完全不同的原因(块用满 vs inode 用满),du 找不到大头时还有第三种(被删除但仍在打开的文件)。这一章把磁盘相关命令按「看容量 → 看谁占 → 看设备 → 挂载 → 修文件系统」的顺序排好。
1. df —— 看文件系统容量
df -h # 各文件系统使用率(人读大小)
df -hT # 含文件系统类型
df -i # inode 使用率——磁盘满的第二种原因
df -h /srv/app # 只看这个路径所在分区
df -h 各列:Filesystem(来源设备或 overlay)、Size(总容量,不含文件系统预留,所以比 lsblk 显示的小)、Used、Avail、Use%(注意它按 Used/(Used+Avail) 算,root 预留的 5% 不计入,所以会出现「Avail 还剩 4.5G 但 Use% 已 100%」)、Mounted on。
2. du —— 看谁占了空间
du -sh /srv/app # 单个目录总占用
du -h --max-depth=1 . | sort -hr | head -15 # 当前目录各子目录大小 Top 15
du -xh / --max-depth=2 2>/dev/null | sort -h | tail -20 # 全盘 Top 大目录(-x 不跨挂载点)
du -sh /var/log/* | sort -h | tail # 按日志类别看
find / -xdev -type f -size +200M -printf '%s\t%p\n' | sort -rn | head # 全盘 Top 大文件(第六章)
sudo lsof +L1 | head # 已删除但仍被占用的空间
docker system df # Docker 占用(docker system prune -a 清理)
du 与 df 数字不一致的三个正当理由:① 有被删除但进程仍打开的文件(du 看不见,df 算满);② 隐藏挂载点覆盖目录(du 算的是覆盖后的);③ 文件系统预留块(df 的 Size 不含)。加 -x 避免扫进别的文件系统导致统计跑偏;--apparent-size 看表观大小(稀疏文件差异巨大)。
3. lsblk、blkid、findmnt —— 看设备与挂载
lsblk -f # 块设备树,含文件系统类型与 UUID
lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINTS # 自选列
sudo blkid /dev/vdb1 # 设备 UUID 与类型——写 /etc/fstab 时用 UUID 而不是设备名
findmnt -T /srv/app # 问这个路径落在哪个挂载点、读写还是只读(ro 就是磁盘只读挂载)
findmnt --fstab # 看 fstab 的解析结果
free -h # 内存与 swap;available 才是可用值(第十七章)
sudo swapon --show # swap 分区/文件;swapon /swapfile 启用、swapoff -a 关闭(容器里通常禁用)
4. inode 用满:另一种「磁盘满」
症状:df -h 显示还有 30% 空间,但写文件报 No space left on device → 跑 df -i,IFuse% = 100% 就是 inode 用满。谁吃的:
for d in /var/spool/* /srv/app/tmp; do echo -n "$d "; find $d -type f | wc -l; done
find / -xdev -type f | awk -F/ '{print $2"/"$3}' | sort | uniq -c | sort -rn | head
find /srv/app/tmp -type f -mtime +1 -delete # 清完 df -i 立刻回落
通常是海量小文件(session、缓存、mail queue、监控点)。根治靠重建分区时 mkfs -T ext4 -i 4096(每 4096 字节一个 inode,默认 16384)或换 XFS/Btrfs——ext4 的 inode 数量在格式化时定死,运行期不能扩;XFS 的 inode 随用随长(受 imaxpct 限制,可 xfs_growfs -m 调)。属于规划问题,需要备份重来。
5. 分区与格式化(危险区,先读警告)
危险边界必须先讲清:mkfs、fdisk/parted 的写操作、dd 都是不可逆的。改分区表前一定 cp /etc/fstab ~/ 并做完整备份;mkfs 之前用 lsblk -f 与 findmnt 双重确认目标设备名(/dev/vdb 与 /dev/vdb1 差一个数字就格式化了整块盘)。
sudo fdisk /dev/vdb # MBR/传统分区:n 新建、d 删除、p 打印、w 写入、q 弃改动
# 只在 w 之前不生效;2TB 以上要用 GPT
sudo parted -s /dev/vdb mklabel gpt # 建 GPT 分区表
sudo parted /dev/vdb mkpart data ext4 1MiB 100% # 建分区——注意 parted 的 mkpart 立即写盘,
# 没有「先看看」的机会;改完 partprobe 通知内核
sudo mkfs.ext4 -L data -m 1 /dev/vdb1 # 建 ext4 文件系统——销毁数据!
# -L 卷标;-m 1 把 root 预留降到 1%
sudo mkfs.xfs -f /dev/vdb1 # 建 XFS:-f 强制覆盖已有签名
sudo wipefs -a /dev/vdb1 # 清设备上的文件系统签名——重建盘前用,极其危险
6. mount、umount 与 /etc/fstab
sudo mount /dev/vdb1 /data # 挂载;挂载点原有内容会被「遮住」(不是丢失),卸载后回来
sudo mount -o ro,noexec /dev/vdb1 /data # 只读 + 禁执行挂载
sudo mount -o remount,rw / # 重新挂载为读写
sudo umount /data # 卸载
sudo umount -l /data # 惰性卸载——报 target is busy 时先用
sudo lsof +D /data # lsof/fuser 找占用者,别习惯性 -l 掩盖问题
sudo fuser -vm /data
sudo systemctl daemon-reload && sudo mount -a # 改完 fstab 用 mount -a 验证:成功才说明重启不会翻车
findmnt --verify # 检查 /etc/fstab 语法——fstab 写错会导致启动进 emergency mode
/etc/fstab 六个字段:
UUID=2f8b… /data ext4 defaults,noatime,nofail 0 2
设备或UUID 挂载点 类型 挂载选项 dump fsck顺序
| 字段 | 常用值 | 说明 |
|---|---|---|
| 1 设备 | UUID=、LABEL=、/dev/vdb1 | 一律用 UUID:设备名会因内核探测顺序变化 |
| 2 挂载点 | none、swap、/data | 目录必须已存在 |
| 3 类型 | ext4、xfs、vfat、nfs、swap | auto 让 mount 猜 |
| 4 选项 | 见下 | 逗号分隔,无空格 |
| 5 dump | 0 | 备份工具用,现代基本都填 0 |
| 6 fsck 顺序 | / 填 1,其他本地盘 2,网络/swap 填 0 | 别给 NFS 填非 0,会卡启动 |
关键挂载选项:nofail(设备不在也继续启动,云主机加数据盘必写,否则盘被摘机就起不来)、noatime(不记录访问时间,显著降低读 IO)、nodiratime、ro/rw、noexec(上传目录必备安全项)、nosuid(同上)、compress(zfs/btrfs)、_netdev(NFS:等网络就绪再挂)、x-systemd.automount(按需挂载)。
7. 「No space left」完整排查
| 步骤 | 命令 | 判断 |
|---|---|---|
| 1 | df -h | 块用满 → 进 2;Use% 未满 → 进 3 |
| 2 | du -xh / --max-depth=2 | sort -h | tail | 找目录大头;日志/docker/临时文件三大来源 |
| 2b | sudo lsof +L1 | sort -k7 -n | tail | du 找不到大头时必查:已删除但句柄未关的文件,df 算它占用;重启对应进程即释放 |
| 3 | df -i | inode 满 → 海量小文件,见第 4 节 |
| 4 | findmnt -T /出错的路径 | 落在 ro 的挂载点上 → 是只读问题不是空间问题 |
| 5 | dmesg | tail -30 | EXT4-fs error / Remounting filesystem read-only → 磁盘故障,见 fsck |
/var/log 撑爆的标准处理:journalctl --vacuum-size=200M、logrotate -f /etc/logrotate.conf 强制跑一次、清 docker 日志 truncate -s 0 /var/lib/docker/containers/*/*-json.log(治本是给 docker 配 log-opts 上限)。
8. fsck —— 为什么不能在挂载时跑
fsck 直接改文件系统元数据。分区处于读写挂载时,内核正在并发写这些结构,检查器看到的是「正在变」的磁盘,会把正常状态判为不一致并「修复」成损坏——结果是文件系统当场报废。因此:
| 情况 | 正确做法 |
|---|---|
| 根分区/已挂载分区要检查 | sudo touch /forcefsck 后重启(老式);或 shutdown -rF;或改 fstab 第 6 字段并用 tune2fs -c 30 /dev/sda1 设次数触发;最稳是救援模式/单用户模式或 Live CD |
| 数据盘可下线 | sudo umount /data && sudo fsck -fy /dev/vdb1 && sudo mount /data |
| 只想「检查不修」 | sudo fsck -n /dev/sda1(对挂载分区也不写,但结果不可靠,仅参考) |
| XFS | xfs_repair 同样必须卸载;XFS 只支持离线修复(xfs_check 已废弃) |
| 看自动检查策略 | tune2fs -l /dev/sda1 | grep -E 'Maximum mount count|Check interval|Last check' |
ext4 的 -y(自动答 yes)适合无人值守,但会「修」掉本该保留的数据;重要盘用不带 -y 的交互模式或先 e2image 做元数据镜像。fsck 跑完找回的碎片在 /lost+found,文件名是 inode 号,可用 find / -inum 123456 反查。
9. inotifywait —— 文件监控与那个著名的上限
inotifywait -m -r -e modify,create,delete,move --format '%w %f %e' /srv/app/uploads
# -m 持续监控、-r 递归、-e 指定事件、--format 输出好解析
inotifywatch -t 60 /data # 统计一段时间内的事件计数
来自 inotify-tools 包(Debian apt install inotify-tools,RHEL 在 EPEL)。监控失效/程序报 inotify_add_watch(...) no space 时,不是磁盘满,是 watch 数超限:
cat /proc/sys/fs/inotify/max_user_watches # 默认常为 8192
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.d/99-inotify.conf
sudo sysctl --system
max_user_instances(每个用户的 inotify 实例数)与 max_queued_events(事件队列长度)也常在 CI/IDE 环境需要调高。注意:NFS/FUSE 挂载点上的 inotify 不工作(远程改动不会通知本机),跨机同步要用轮询或 rsync。
踩坑提醒
- 删了大文件空间没回来:先想「有没有进程还开着它」(
lsof +L1),重启进程或: > 文件截断,而不是再删一遍。 rm -rf删目录树时正在被进程写入的日志会导致磁盘占用反而上升(inode 与块都被打开句柄占住),标准清理顺序是先停服务再清。mount到非空目录会「隐藏」原内容,看起来数据丢了其实还在——umount就回来。- 云主机加盘后忘了
nofail,运维把盘摘掉机器直接起不来进 emergency mode。这是最常见的线上事故之一。 dd用于写盘时(dd if=iso of=/dev/sdb)输错设备名就是毁盘,永远先用lsblk -f确认,再三检查of=。
十五、软件包管理:apt、dnf、apk
包管理器的差异是发行版之间最大的日常摩擦:同一种需求(查文件归属、装某个包、看谁依赖它)在 Debian、RHEL、Alpine 上命令完全不同。这一章按体系列命令,然后把「三个反向查询」单独讲透——它们才是排障时真正要找的东西。
1. apt —— Debian / Ubuntu 系
sudo apt update # 刷新索引——只刷新列表,不升级!装任何东西之前
sudo apt install -y nginx # 安装;-y 给脚本用。apt 交互式优先;脚本里用 apt-get(apt 会警告接口不稳定)
sudo apt upgrade # 升级全部(保守,不删包)
sudo apt full-upgrade # 升级并允许删包(旧名 dist-upgrade),大版本迁移用
sudo apt remove apache2 # 删包但留配置;purge 连配置一起删,重装时配置会被读回来
sudo apt autoremove --purge # 清理不再需要的依赖——会带走你手动装的孤儿包,看清楚列表再 yes
apt search nginx # 搜包名与描述(子串不是正则)
apt show nginx # 版本、依赖、大小、源——装之前先看
apt list --installed # 已装列表;可 apt list --installed 'libssl*'
apt policy nginx # 候选版本与来源仓库——排「为什么装到旧版本」的唯一正解(含 Pin 优先级)
apt-cache madison nginx # 各仓库里可用的版本清单
sudo apt-mark hold linux-image-generic # 钉住版本不升级(unhold 解开)——生产锁内核的标准做法
sudo apt -s upgrade # -s 模拟:看会动什么,不实际改动——生产升级前先跑这个
sudo apt -f install # 修依赖:dpkg -i 装失败后的正确一步
2. dpkg —— 本地包与文件归属
sudo dpkg -i ./pkg.deb # 装本地 .deb——不解析依赖!缺依赖时 sudo apt -f install 补
dpkg -l | awk '$2 ~ /^i/ {print $2}' # 只列在用的;rc 状态 = 已删但有残留配置
dpkg -L nginx # 这个包装了哪些文件——找配置/二进制在哪
dpkg -S /etc/nginx/nginx.conf # 这个文件属于哪个包
sudo dpkg-reconfigure tzdata # 重跑配置问答(locales、keyboard-configuration 同理)
apt-file search bin/convert # 查「未装的包」里谁有这个文件(apt install apt-file + apt-file update)
3. dnf 与 rpm —— RHEL / CentOS / Rocky / Fedora 系
dnf 是 yum 的继任者,命令基本兼容(yum 现在通常是 dnf 的软链接):
sudo dnf -y install nginx # 安装;dnf remove pkg 卸载(dnf autoremove 清依赖)
dnf update --security # 只打安全补丁;update-minimal 同义
dnf search log / dnf info nginx # 搜 / 详情
dnf list available 'python3*' # 列可用包
dnf provides '*/nginx.conf' # 哪个包提供这个文件路径——排缺库必备,比 apt-file 方便
dnf repoquery --installed --whatrequires glibc # 谁依赖它(删之前看)
dnf repoquery -l pkg # 包装了哪些文件(等价 rpm -ql)
sudo dnf history # 事务历史;dnf history info 12 看详情
sudo dnf history undo 12 # 回滚——yum 做不到,dnf 的最大优势
dnf module list php # RHEL8+ 模块流:同一包多版本;dnf module enable php:8.2
sudo dnf install dnf-plugins-core # 拿到 dnf config-manager(--add-repo URL)、builddep
rpm -q nginx # 是否已装;rpm -qa 'nginx*' 模糊查
rpm -ql nginx # 包内文件列表;--scripts 连安装脚本一起看
rpm -qf /etc/my.cnf # 文件属于哪个包(对应 dpkg -S)
rpm -qpi pkg.rpm / rpm -qpl pkg.rpm # 查未安装的 rpm 的信息/文件清单
sudo rpm -ivh pkg.rpm # 安装(i 安装 v 显示 h 进度条);依赖不满足会拒绝,改用 dnf install ./pkg.rpm
rpm -V openssh-server # 校验文件是否被改过:S5(大小/摘要不符)、M(模式变)、?(缺失)
sudo rpm -e pkg # 卸载并做依赖检查;--nodeps 强删会留下坏系统,别用
4. apk —— Alpine(容器里最常见)
apk add --no-cache curl bash # 安装;--no-cache 免手动 update,镜像构建里标配
apk del .build-deps # 卸载;多阶段构建里的假想依赖组
apk info -e curl # 存在则返回 0,脚本判断用
apk search -x 'curl*' # 搜包;-x 精确名
apk policy busybox # 看版本与仓库来源
apk info -w /bin/sh # 文件属于哪个包 → busybox
apk info -L pkg / apk info -d pkg # 包内文件 / 包的依赖
apk audit # 检查被改动的受管文件——容器镜像审计
apk fix # 修复损坏安装
5. 三个反向查询:跨体系对照
| 我要问的问题 | Debian/Ubuntu | RHEL 系 | Alpine |
|---|---|---|---|
| 这个文件是谁装进来的 | dpkg -S /usr/bin/convert | rpm -qf /usr/bin/convert | apk info -w /usr/bin/convert |
| 这个包装了哪些文件 | dpkg -L imagemagick | rpm -ql imagemagick | apk info -L imagemagick |
| 某个命令在哪个包里(还没装) | apt-file search bin/convert 或 apt search(或直接装 debscan) | dnf provides '*/convert'(最方便,仓库索引里查) | apk search -x + 看包描述 |
| 谁依赖这个包 | apt-cache rdepends pkg | dnf repoquery --whatrequires --installed pkg | apk info -t pkg(依赖树反向) |
| 这个包依赖什么 | apt-cache depends pkg / dpkg -s pkg | dnf repoquery --requires pkg | apk info -d pkg |
| 文件有没有被改过 | debsums(要装) | rpm -V pkg(内置) | apk audit |
「这个命令找不到,该装哪个包」在 Debian 上还有个内置提示器:装 command-not-found(Ubuntu 默认有)后,输入 convert 会直接回 sudo apt install imagemagick。RHEL 系同理由 dnf-plugins-core 提供。
6. 源码安装
tar xzf app-1.9.3.tar.gz && cd app-1.9.3
./configure --prefix=/usr/local --with-openssl # ① 探测环境,生成 Makefile
make -j"$(nproc)" # ② 编译
sudo make install # ③ 装到 /usr/local
sudo ldconfig # ④ 刷新动态库缓存(装了 .so 才需要)
| 步骤 | 常见失败 | 处理 |
|---|---|---|
./configure | no acceptable C compiler / 缺依赖头文件 | apt install build-essential;缺的 -dev 包按报错补(zlib1g-dev、libssl-dev);RHEL 系 dnf groupinstall "Development Tools";dnf builddep pkg.src.rpm / apt build-dep pkg 一次装齐构建依赖 |
make | 内存不足被 OOM 杀掉 | 降到 make -j1;或加 swap |
make install | 没权限写 /usr/local | 用 --prefix=$HOME/.local(之后要把 $HOME/.local/bin 加 PATH) |
| 运行期 | error while loading shared libraries: libxxx.so.1 | 库没在 /etc/ld.so.conf 路径里,echo /usr/local/lib | sudo tee /etc/ld.so.conf.d/local.conf && sudo ldconfig;临时验证用 LD_LIBRARY_PATH |
| 卸载 | make uninstall 通常不存在 | 这正是源码安装的致命缺陷 |
checkinstall 的价值:把 make install 的产物打包成一个受包管理器记录的 .deb/.rpm,于是 dpkg -L 能查文件、apt remove 能干净卸载。命令替换:sudo checkinstall --pkgname=myapp --pkgversion=1.9.3 --default。原则:能用发行版包就不源码装;必须源码装就用 checkinstall,并只装到 /usr/local(与系统包目录隔离,方便整体删除)。
7. snap / flatpak / appimage
| 新型包格式 | 命令 | 一句话 |
|---|---|---|
snap(Ubuntu 主推) | snap install htop、snap list、snap changes、snap revert | 自带依赖与沙箱,自动升级;冷启动慢、/snap 占空间大、镜像源在某些网络下很慢 |
flatpak(桌面应用) | flatpak install flathub org.videolan.VLC、flatpak run …、flatpak update | 面向桌面 GUI,服务器基本不用 |
appimage | ./SomeApp.AppImage | 单文件免安装,给一个可执行位就能跑 |
pipx / cargo install / npm i -g | 语言级生态 | 与系统包管理器隔离,冲突少但更新不统一 |
8. 容器化发行版为什么没有 systemd
Docker/OCI 镜像里的「Debian/Alpine」只是一个根文件系统,不是完整虚拟机:
| 现象 | 原因 | 后果与替代 |
|---|---|---|
没有 systemctl(或报 System has not been booted with systemd) | PID 1 是应用本身,没有 init 系统 | 用 docker exec 起进程、镜像 entrypoint、s6/supervisord 管多进程 |
apt 装完服务不自动启动 | 没有 systemd 触发 unit | 手工在启动脚本里拉起,或改由 supervisor 管 |
journalctl 无用武之地 | journald 没跑 | 日志走 stdout/stderr,由 docker logs / kubectl logs 收 |
| cron 不工作 | crond 没被启动 | 外部调度器(K8s CronJob)或镜像里显式起 crond |
ip/ss 看不到宿主机全部信息 | 独立网络命名空间 | nsenter 进命名空间,或在容器内只关心自己那套 |
| 装包习惯差异 | 镜像层要越小越好 | apt install -y --no-install-recommends、apk add --no-cache、装完 && rm -rf /var/lib/apt/lists/*;调试工具单独打 debug 镜像 |
排「容器里为什么这条命令没有」时,先想是不是精简镜像(*-slim、alpine、distroless)根本没带这个包,用 apk info / dpkg -l 核对,再决定是装包还是换镜像。
踩坑提醒
apt upgrade与apt full-upgrade混用能引出依赖地狱;生产上先apt -s upgrade(-s模拟,不实际改动)看会动什么。- 手动
dpkg -i装失败后不要反复重试,sudo apt -f install才是修依赖的正确一步。 rpm -Va输出巨多不等于系统被入侵:绝大多数是配置文件被管理员合法改动、日志文件被写入。看S5(摘要变了)且是二进制路径才值得警惕。- 升级 Python/OpenSSL/glibc 这类系统库时永远不要用源码覆盖(
make install到/usr),apt/dnf 自身依赖它们,覆盖会当场把包管理器变砖。要装新版本用apt install python3.12、dnf module、pyenv(装在用户目录)或容器。
十六、Shell 本身:变量、引号与脚本的标配
bash 不是命令行集合,它是一门语言。脚本里 90% 的诡异行为(变量为空、单词被拆、退出码丢失)都出自三个地方:引号、展开、set -e 的缺失。这一章把这几件事一次讲清。
1. 引号三兄弟
| 写法 | 名字 | 会不会展开变量/命令 | 例子 |
|---|---|---|---|
'…' | 单引号 | 完全不展开,所见即字面 | echo '$HOME $(date)' 原样输出 |
"…" | 双引号 | 展开 $var、$(cmd)、`,但不拆分单词、不做 glob | echo "$file" 带空格安全 |
`…` | 反引号命令替换 | 展开,但嵌套需转义,可读性差 | 新代码一律换成 $(…) |
规则一句话:变量永远包在双引号里("$x"),除非你明确想要单词拆分。判断是否会被拆,用 set -f(临时禁 glob)或 printf '[%s]\n' "$x" 观察。
$'…' 与 $"…":$'a\nb' 支持 C 转义(\n、\t、\x41);$"msg" 做本地化翻译(基本遇不到)。空数组/未设变量在 set -u 下的安全写法是 ${x:-}。
2. 变量与 export
NAME=deploy # 赋值——等号两边不能有空格!NAME = deploy 会被当成执行命令
readonly PI=3.14 # 只读,再改会报错
export JAVA_HOME=/usr/lib/jvm/java-17 # 导出为环境变量(子进程可见);只对之后的子进程生效
export -f fn # 导出函数给子 bash;cron/systemd 里不生效(不是 bash 子进程)
env # 打印当前全部环境;env -i bash 起一个干净环境(测试利器)
printenv PATH # 取单个变量,比 echo $PATH 稳(未设时 echo 输出空行看不出来)
unset NAME # 删变量;export -n NAME 只取消导出而保留本地值
declare -p # 看变量的类型与属性,排查数组/整数属性时用
变量展开的形式(脚本里避免 bug 的关键):
| 写法 | 含义 |
|---|---|
${x:-默认} | x 未设或空则用默认(不改变 x) |
${x:=默认} | x 未设或空则赋值为默认(常用于「首次运行算一次」) |
${x:?消息} | x 未设或空则报错退出——破坏性命令前的保险:rm -rf "${DIR:?}/*" |
${x:+替代} | x 有值才用替代值 |
${#x} / ${x:5:3} / ${x%%.txt} / ${x//a/b} | 长度 / 子串 / 删后缀 / 替换(比调用 sed 快且无 fork) |
3. PATH 怎么安全地加一段
export PATH="$HOME/.local/bin:$PATH" # 日常写法;重复执行会把 PATH 越拉越长,脚本里别这样
case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) export PATH="$HOME/.local/bin:$PATH" ;; esac
# 幂等写法:重复执行不累积
坑:PATH 里出现空段(开头或结尾的 :,或 PATH=:)等价于把当前目录 . 加进搜索路径——任何人能在 /tmp 放一个 ls 骗你执行。检查:echo "$PATH" | tr ':' '\n' | nl。sudo 会用 secure_path 覆盖你的 PATH(sudo -V | grep secure_path 查看),所以 sudo mytool 找不到东西时要改 /etc/sudoers 而不是 export。
4. 启动文件什么时候被读
| 文件 | 谁读它 | 何时 | 放什么 |
|---|---|---|---|
/etc/profile → /etc/profile.d/*.sh | 登录 shell | 登录时一次 | 系统级环境(模块、语言、代理) |
~/.bash_profile / ~/.profile / ~/.login | 登录 shell | 找到第一个就不往后(bash 只看 .bash_profile、.bash_login、.profile) | 只写 export,并 source ~/.bashrc |
~/.bashrc | 非交互但交互式的 shell(每个新终端窗口) | 每次开 shell | alias、函数、提示符、补全 |
/etc/bash.bashrc / /etc/bashrc | 所有 bash | 每次 | 系统级 alias |
~/.bash_logout | 登录 shell | 退出时 | 清理(清屏等) |
$BASH_ENV | 非交互 shell(脚本) | 脚本启动时 | 极少用,注意脚本环境 |
经典问题:.bashrc 里写了 alias 但 ssh host 'cmd' 不生效——因为 ssh host cmd 起的是非交互 shell,.bashrc 开头通常有 case $- in *i*) ;; *) return;; esac 直接返回。把要共享的东西放 .profile/.bashrc 的守卫之前,或用 BASH_ENV。
5. alias、函数与 history
alias ll='ls -lh --color=auto' # 定义别名;别名不做参数展开,需要参数就写函数
unalias ll # 取消;\ls 或 command ls 临时绕开别名;脚本里判断用 type -a
mkcd() { mkdir -p "$1" && cd "$1"; } # 函数:定义即执行副作用
history # 历史列表;history 20 看最近 20 条;HISTSIZE/HISTFILESIZE 控条数
history -a # 立即落盘;多终端并发会互相覆盖,设 shopt -s histappend
!! # 重复上条整条;sudo !! 是忘加 sudo 的标准补救
!$ # 上条的最后一个参数:mkdir a/b/c 然后 cd !$;!* 是全部参数
!42 # 执行历史第 42 条;!grep 按开头字符串;!?conf? 最近含 conf 的
!42:p # 只打印不执行,先确认再跑
Ctrl-R # 反向搜索——效率最高的一个键;配 fzf 后可模糊选
history -c; history -w # 清历史;注意 ~/.bash_history 里的敏感命令
位置参数与特殊变量:
| 变量 | 含义 | 关键区别 |
|---|---|---|
$0 | 脚本名 | 用 basename "$0" 做 usage 提示 |
$1…$9、${10} | 第 N 个参数 | 10 个以上必须加花括号 |
$# | 参数个数 | 判断数量:[ $# -lt 2 ] && usage |
"$@" | 所有参数,各自保持为独立词 | 正确默认写法;for f in "$@"、cmd "$@" |
"$*" | 所有参数拼成一个字符串(按 $1 分隔) | 只在「要当成一整串」时用 |
$$ | 当前 shell PID | 临时文件命名 f=$(mktemp) 更好 |
$? | 上一条命令的退出码 | 0 成功;set -e 之后基本不用再手动查 |
$! | 最后一个后台进程 PID | cmd & pid=$! 之后 kill $pid |
$_ | 上一条命令的最后一个参数 | 交互式很好用 |
$- / $BASHPID | 当前选项标志 / 子 shell 真实 PID | 判断是否交互 shell |
6. 脚本的标配
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
| 选项 | 作用 | 为什么是标配 |
|---|---|---|
set -e(-o errexit) | 命令失败立即退出 | 没有它,脚本会在错误状态上继续跑到底(cd 失败后继续 rm) |
set -u(-o nounset) | 引用未定义变量即报错 | 抓拼写错误与「本该传参却忘了传」 |
set -o pipefail | 管道里任一命令失败即失败 | 默认只看最后一个命令的退出码:grep x file | wc -l 里 grep 失败被吞 |
set -x(-o xtrace) | 打印每条命令的实际展开 | 调试首选,等价临时 bash -x script.sh |
set -v | 打印读入的原始行 | 看解析问题 |
set +x | 关掉上面的跟踪 | 遇到密码时记得关 |
IFS=$'\n\t' | 限制字段分割符 | 防文件名里的空格被拆 |
set -e 的例外(很多人踩):条件语句里的命令(if cmd; then)、&&/|| 左侧、管道里非最后一个命令(无 pipefail 时)、以及被取反的 ! cmd 都不会触发退出。函数返回值看最后一条命令。
7. [[ ]] 与 ,以及数组
| 维度 | [ ](POSIX test) | [[ ]](bash 关键字) |
|---|---|---|
| 词拆分/ glob | 会(变量必须加引号) | 不会,[[ $x == *.log ]] 安全 |
| 正则匹配 | 无 | [[ $f =~ ^[0-9]{3}$ ]](右侧不要加引号) |
| 逻辑组合 | 只能用 -a/-o(有坑) | 支持 && 与逻辑或(双竖线),可嵌套 |
| 字符串比较 | =(== 也接受但非 POSIX) | == 支持模式匹配 |
| 未加引号的空变量 | [: too many arguments 报错 | 正常处理 |
| 可用性 | 所有 sh | bash/zsh/ksh(#!/bin/sh 里不能用) |
结论:bash 脚本一律 [[ ]],/bin/sh 脚本一律 [ ] 并给变量加引号。文件测试通用:-e 存在、-f 普通文件、-d 目录、-x 可执行、-r/-w、-s 非空、-L 符号链接、a -nt b 更新。
数组与关联数组:
files=(a.log b.log c.log)
echo "${files[1]}" # b.log
echo "${#files[@]}" # 3(元素数)
printf '%s\n' "${files[@]}" # 全部展开,各占一行
files+=(d.log) # 追加
unset 'files[1]' # 删元素(注意引号,否则 shell 先展开)
declare -A seen # 关联数组(bash 4+,Alpine 的 ash 不支持)
seen[10.0.0.12]=42
for k in "${!seen[@]}"; do echo "$k => ${seen[$k]}"; done
(( seen[10.0.0.12]++ )) # 计数惯用式
8. shebang、执行方式与 read
| 写法 | 含义 |
|---|---|
#!/bin/bash | 固定用 bash(发行版路径一致时最简单) |
#!/usr/bin/env bash | 从 PATH 找 bash,跨系统更稳(Nix/自编译场景必备) |
#!/bin/sh | 承诺只用 POSIX(此时不能 [[ ]]、不能用数组) |
#!/usr/bin/env -S bash -eu | env 的 -S 把参数一起传(GNU coreutils 8.30+) |
| 没有 shebang | 由调用者决定;sh script.sh 会按 POSIX 跑,bash 语法当场报错 |
chmod +x deploy.sh # 加执行位(没有它 ./deploy.sh 报 Permission denied)
bash deploy.sh # 显式用 bash 跑,不需要执行位
source deploy.sh # 在当前 shell 执行(cd 与 export 会留下来);. ./env.sh 是 POSIX 写法
./deploy.sh # 子 shell 执行(最常见:改了 PATH 却没生效,就是用了 ./)
read -p "目标环境 [dev/prod]: " env # 提示 + 读一行到 env
read -r -d '' big < file # 读整个文件(-d '' 以 NUL 结束)
read -s -p "密码: " pass < /dev/tty # -s 不回显;从 tty 读而非管道
printf '%s\n' a b c | while IFS= read -r line; do …; done # 逐行读的正确姿势
read -t 5 -n 1 -p "按任意键继续" _ # 带超时与单字符
read 的两个必备参数:-r(否则反斜杠被吃掉)、配 IFS=(否则行首尾空白被去)。逐行处理文件时不要用 for line in $(cat file)——它会按空白拆且丢失空行。
踩坑提醒
set -e与if/&&组合时不生效是设计,不是 bug;关键步骤显式判返回值或加|| exit 1。"$*"与"$@"写反了不会报错,只会把带空格的参数粘成一坨——这是脚本处理文件名出问题的第一来源。- 反引号里的
$( )混用导致转义地狱:全文统一$()。 - 修改
PATH/cd之后在下一个命令里「没变化」,几乎一定是因为你用了./script.sh(子 shell)而不是source。 .bashrc里写了set -e会让交互 shell 在命令失败后直接退出——这是很危险的误配,选项只该写在脚本里。- 用
echo输出以-开头的内容会被当参数:统一用printf '%s\n'。
十七、性能与排障:free、iostat、strace 与总速查表
机器慢、服务挂、磁盘满、连不上——排障的通用方法是「用一小组命令把每个资源维度各看一眼」,而不是凭猜测乱翻。这一章先给每个资源的观测命令,再给一张「现象 → 先跑哪几条」的映射表,最后收口成「想干一件事 → 用什么命令」的总速查表。
1. free —— 内存:available 才是重点
free -h
total used free shared buff/cache available
Mem: 7.7Gi 3.1Gi 214Mi 18Mi 4.3Gi 4.2Gi
Swap: 1.9Gi 128Mi 1.8Gi
| 列 | 含义 | 读法 |
|---|---|---|
total | 物理内存总量 | 减去内核占用才是可用上限 |
used | 应用真正占用 | used = total - free - buff/cache(不同版本算法略有差异) |
free | 完全没被用过的内存 | 小是正常的,Linux 会把空闲内存拿去做页缓存 |
buff/cache | 块设备缓冲 + 页缓存 | 可随时回收,看着大不是问题 |
available | 估算「还能给新应用多少」(free + 可回收的 cache) | 判断内存够不够只看这一列 |
shared | tmpfs(含 /dev/shm)占用 | 变大会真吃掉内存 |
「内存吃满」的判据:available 很小 且 Swap 开始进出(vmstat 的 si/so 非 0)。只看 free 那一列得出「内存不足」的结论是错的。查谁在用:ps -eo pid,user,rss,cmd --sort=-rss | head(RSS 单位 KB)、smem -rk(含 PSS,共享内存分摊更准)、sudo systemd-cgtop memory(按 cgroup 看,容器环境最准)。
2. uptime —— 负载:要结合核数看
uptime # 09:42:11 up 14 days, 3:29, 2 users, load average: 1.85, 1.42, 0.98
nproc # 4
| 观察 | 判读 |
|---|---|
| 1 / 5 / 15 分钟三个数 | 短期抖动看第一个;持续升高看第三个;15 分钟值高才是真有问题 |
| 与核数之比 | load / nproc ≈ 1 表示刚好排满;4 核上 load 8 = 一半时间在排队;load 1 在 4 核上算很闲 |
高 load 但 %CPU 不高 | 说明在等 D 状态(磁盘/NFS)——不是 CPU 不够,去查 IO |
| 容器里的 load | 显示的是宿主机的,不能用它判断容器健康;看 docker stats 或 cgroup 的 cpu.stat |
CPU 各时间的细分来源:us(应用代码)、sy(内核,sy 高常见于频繁上下文切换/系统调用风暴)、wa(等 IO)、si/hi(软/硬中断,网络包风暴会打高 si)、st(被宿主机「偷走」的时间,云主机常见)。工具:top/htop、mpstat -P ALL 1(每核分布)、pidstat 1(每进程)、perf top(热点函数,需 kernel 权限)、cgroup 限额导致的 throttled_time。
3. iostat、sar、vmstat —— 磁盘 IO
| 工具 | 命令 | 关键列 |
|---|---|---|
iostat(sysstat 包) | iostat -xz 1 | %util(设备繁忙比例,接近 100 说明设备是瓶颈)、await(平均等待 ms,SSD 应 <1,HDD <10)、r_await/w_await 分开看、avgqu-sz(队列深度) |
sar | sar -d 1 3(磁盘)、sar -u 1(CPU)、sar -r 1(内存)、sar -n DEV 1(网卡)、sar -B 1(换页) | 能查历史:sar -u -f /var/log/sa/sa04,排「昨天 3 点发生了什么」的唯一实用工具 |
vmstat | vmstat 1 | r(运行队列)、b(IO 阻塞数,持续 >0 说明 IO 堵)、si/so(swap 活动)、wa |
iotop | sudo iotop -o | 哪个进程在读写(-o 只显示有 IO 的) |
pidstat -d 1 -p PID | 每进程读写 KB/s | — |
blktrace/biosnoop | BCC/bpftrace | 内核级追踪,深度排障 |
%util 对 SSD/NVMe 阵列不再等于饱和(它能并行),要看 await 与队列深度。sar 的历史数据需要 sysstat 服务开启(cat /etc/sysstat 或 ENABLED=yes,RHEL 默认开)。
4. dmesg —— 内核消息与 OOM Killer
dmesg -T | tail -50 # 人类时间戳看最近内核消息
dmesg -w # 持续看,等价 journalctl -kf
dmesg | grep -iE 'killed process|out of memory' # 找 OOM:内存耗尽被杀的记录
journalctl -k --since "1 hour ago" | grep -i oom # 同上,走 journald
dmesg | grep -iE 'error|I/O|reset|fail' # 硬件错:EXT4-fs error、nvme: controller down
OOM 记录会写清是谁被杀:Out of memory: Killed process 28414 (java) total-vm:…。保护关键进程:echo -1000 > /proc/$(pgrep app)/oom_score_adj(范围 -1000~1000),systemd 里写 OOMScoreAdjust=。容器被杀看 cgroup:docker inspect --format '{{.State.OOMKilled}}' id、cat /sys/fs/cgroup/.../memory.events 的 oom_kill。
OOM 之后要做的:确认是谁涨的(ps 按 RSS + 应用日志)、给服务加 MemoryMax=(systemd)或容器 --memory,避免它把整机拖死;vm.swappiness=10 让内核更愿意用 swap 而不是直接杀(不是根治)。
5. lsof —— 谁在用这个文件/端口
sudo lsof -i :8080 # 按端口找进程;-i TCP:8080、-i 4(仅 IPv4)、-sTCP:LISTEN 只看监听
sudo lsof /srv/app/app.log # 按文件找进程——排「umount busy」「空间不回收」都用它
sudo lsof +L1 # 已删除但被占用:链接数 <1 的文件,磁盘空间被它占着
sudo lsof -u deploy # 按用户:看某人开了什么
sudo lsof -p 28414 # 按 PID 全量:列它的 fd、mmap、库、socket
sudo lsof -i # 只看网络,与 ss 互补(能看到 socket 文件描述符号)
lsof | wc -l # 排查 fd 泄漏:ls /proc/PID/fd | wc -l 对比 ulimit -n
lsof 慢(要扫所有进程的 fd),生产上先看 ss//proc/PID/fd,需要精确定位再 lsof。fuser -vm /data 是它的轻量替代(fuser -k /data 直接杀占用者——慎用)。
6. strace / ltrace —— 卡在哪个系统调用
strace -f -tt ./app # 从头跟踪:-f 含子进程、-tt 带时间戳
strace -p 28414 -f -s 256 # 附加到运行中的进程(-s 256 加长字符串打印)
strace -e trace=network,file -p PID # 只看某类调用(-e trace=%file)
strace -c -p 28414 # 只出统计:哪个调用耗时/次数最多——排「慢」先看这个
strace -o /tmp/st.log -yy -ttt -f -p PID # 落盘分析(-yy 给 socket 对端地址)
strace -e openat -f bash script.sh # 「脚本到底在读哪个配置文件」的终极答案
ltrace -c -p PID # 跟踪库函数调用(看它调了哪个 printf/SSL_read)
perf trace # 更低开销的 strace(bpf 实现)
strace 会让目标进程明显变慢(每次系统调用都要停下与 ptrace 通信),生产环境用 -c 抽样或短时间附加,别长时间挂着。看 EACCES/ENOENT/EAGAIN/ETIMEDOUT 这几个错误码,通常一步到位定位问题。ltrace 对静态编译的程序无效(没有库调用可跟踪)。
7. tcpdump 与实时流量
sudo tcpdump -i any -nn tcp port 8080 # 看某端口有没有流量
sudo tcpdump -i eth0 -nn host 10.0.0.5 and port 5432 # 抓某主机与某服务的全部包
sudo tcpdump -i any -w /tmp/lost.pcap tcp port 443 # 抓下来给 Wireshark 分析(-W 5 -C 50 分片限大小)
sudo tcpdump -nn 'tcp[tcpflags] & (tcp-syn) != 0 and dst port 443' # 只看 SYN 无响应(被防火墙丢)
sudo tcpdump -nn 'tcp[tcpflags] & tcp-rst != 0' # 看谁在连不上时发 RST
sudo tcpdump -nn -s0 -A port 53 # 抓 DNS 问答
参数要点:-nn(不解析端口/主机名,避免自己造 DNS 流量并卡住)、-i any(跨网卡,容器/桥接场景必备)、-s 0(抓完整包,老版本默认截 96 字节)、-c 100(限量防写爆磁盘)、-A/-X(看载荷,明文协议调试用)、-l(行缓冲实时输出,配管道用)。看实时流量分布用 nethogs(按进程看带宽,找「谁在下东西」最直接)或 iftop -P(按连接看,-i eth0,界面里 p 显示累计、b 切换源/目的排序)。
8. 现象 → 先跑哪几条命令
| 现象 | 第一批命令(照抄) | 看到什么算定位 |
|---|---|---|
| CPU 高 / 变慢 | uptime; nproc → top(按 P)→ pidstat 1 5 → strace -c -p PID | us 高 = 应用算法/循环;sy 高 = 系统调用风暴;wa 高 = 其实是 IO;load 高但 CPU 空 = D 状态堆 |
| 内存吃满 / 被 OOM | free -h → dmesg | grep -i kill → ps -eo pid,user,rss,cmd --sort=-rss | head → cat /proc/meminfo | head | available 小 + si/so 活动 = 真缺;SReclaimable/buff/cache 大 = 假象 |
| 磁盘满 | df -h → df -i → du -xh / --max-depth=2 | sort -h | tail → sudo lsof +L1 | 三者各对应:块满 / inode 满 / 已删除未释放 |
| 磁盘 IO 高 | iostat -xz 1 → iotop -o → pidstat -d 1 | %util≈100 且 await 大 = 设备瓶颈;util 低但 await 高 = 队列排队/驱动 |
| 网络不通 | ip a → ip route get 目标IP → ping -c 3 → nc -vz 目标 端口 → curl -v | 路由不通 / ICMP 被封 / TCP 连不上 / TLS 或 HTTP 层错,四层各归各位 |
| 端口没监听 | ss -tulnp → ss -lntp | grep 8080 → systemctl status 服务 | 根本没起 vs 只绑了 127.0.0.1 vs 被别的服务占了 |
| 服务起不来 | systemctl status 名 → journalctl -u 名 -n 50 --no-pager → systemctl cat 名 | 看 Main PID、Exit code、ExecStart 路径是否存在与权限 |
| 进程杀不掉 | ps -o pid,stat,user,cmd -p PID → sudo kill 15 → sudo kill -9 → 若 D 状态:cat /proc/PID/wchan | D = 等内核 IO(杀不动,修底层);Z = 僵尸(父进程回收);systemd 拉起 = 用 systemctl stop |
| 时间不对 | timedatectl → chronyc sources/ntpq -p → hwclock -r | NTPSynchronized=no、时区错、双系统 UTC/本地冲突 |
| 权限被拒 | sudo -u appuser 原命令 → namei -l 全路径 → id appuser → getenforce/ausearch -m avc | 父目录缺 x、组变更后没重登、SELinux 上下文 |
| 文件找不到/版本乱 | command -v X → dpkg -S $(command -v X)(或 rpm -qf)→ update-alternatives --list X | 多版本共存时看谁在前 |
| 登录不上 SSH | ssh -v → journalctl -u ssh -n 50 → sudo ss -tlnp | grep 22 → sudo tail -50 /var/log/auth.log | 端口、配置、认证方式、faillock 各查一段 |
| 某个应用行为怪 | strace -f -e trace=file,network ./app → lsof -p PID | 读错配置、连错地址、写到只读目录 |
9. 总速查表:我想干一件事 → 用什么命令
| 我想 | 命令 |
|---|---|
| 看我在哪、有什么 | pwd、ls -lh、tree -L 2 |
| 找文件(按名字) | find / -xdev -maxdepth 4 -name '*.log' -mtime -7 |
| 找文件(按内容) | grep -rl '关键词' /etc |
| 找命令属于哪个包 | dpkg -S $(command -v X) / rpm -qf $(command -v X) |
| 看文件内容 | less -N file、head -n 200、tail -F file |
| 看不可见字符/换行 | cat -A file、od -c -N 64 file |
| 批量替换配置 | sed -i.bak 's/^旧/新/' file 然后 diff file.bak file |
| 按列统计 | awk '{print $1}' log | sort | uniq -c | sort -rn | head |
| 看谁占空间 | du -h --max-depth=1 . | sort -h | tail -15 |
| 看磁盘/inode | df -hT、df -i |
| 看挂载与读写状态 | findmnt -T /路径、lsblk -f |
| 看进程 | ps aux --sort=-%cpu | head、top、htop |
| 按名字找进程 | pgrep -af '关键词' |
| 停服务 / 停进程 | systemctl stop app;kill 28414 → 10 秒后 kill -9 28414 |
| 看服务日志 | journalctl -u app -f、journalctl -u app --since "10 min ago" -p err |
| 改服务配置 | systemctl edit app → systemctl daemon-reload && systemctl restart app |
| 看谁在监听端口 | sudo ss -tulnp |
| 测端口通不通 | nc -vz 10.0.0.5 5432 |
| 测接口健康 | curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' URL |
| 验证 DNS | getent hosts 域名、dig +short 域名 @1.1.1.1 |
| 走跳板连内网 | ssh -J bastion app、ssh -L 5432:db:5432 bastion |
| 备份/同步目录 | rsync -avz --delete src/ host:/backup/(先 -n 干跑) |
| 打包 / 解包 | tar czf app.tar.gz 目录、tar xzf app.tar.gz -C 目标 |
| 不解压搜日志 | zgrep -h 'ERROR' /var/log/*.gz |
| 加一条定时任务 | crontab -e(命令写绝对路径、输出重定向、% 转义) |
| 建用户/加组 | sudo useradd -m -s /bin/bash 名字、sudo usermod -aG docker 名字(必须 -a,然后重新登录) |
| 看自己身份 | id、groups、sudo -l |
| 修权限 | chown -R app:app /srv/app、chmod -R a+rX /srv/app、namei -l 全路径 |
| 装/卸包 | apt install -y pkg / dnf -y install pkg / apk add --no-cache pkg |
| 查包版本候选 | apt policy pkg / dnf list pkg |
| 看资源总览 | free -h、uptime、iostat -xz 1、vmstat 1、vgs/lvs(LVM) |
| 查历史性能 | sar -u -f /var/log/sa/sa04 |
| 抓包 | sudo tcpdump -i any -nn -c 200 tcp port 443 |
| 定位「卡在哪个调用」 | strace -c -p PID、strace -f -e trace=file ./app |
| 看内核报错 | dmesg -T | tail -50、journalctl -k -b --since "1 hour ago" |
| 跑个脚本看细节 | bash -x script.sh、脚本头 set -euo pipefail |
| 临时改环境变量 | export PATH="$HOME/.local/bin:$PATH"、env -i bash 起干净环境 |
| 查/改时间与时区 | timedatectl、timedatectl set-timezone Asia/Shanghai、timedatectl set-ntp true |
| 安全地删一批文件 | find dir -name '*.tmp' -mtime +3 -print 确认后再把 -print 换 -delete |
一句话记住
排障永远按资源维度各看一眼:CPU(uptime/top)→ 内存(free/dmesg)→ 磁盘容量(df -h / df -i / du)→ 磁盘 IO(iostat)→ 网络(ip/ss/curl)→ 服务(systemctl/journalctl)→ 权限(namei/id)。七条命令走完还定位不到,才需要 strace 和 tcpdump 这种深度工具。本手册第十七张表就是这张检查单的落地版本。