Linux 常用命令手册:按场景查的命令清单

一、开始敲命令之前:终端与帮助系统

在 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 aliascd is a shell builtin由 Shell 进程自己实现,改不了当前目录这类事的只能做成内建
外部命令(可执行文件)ls grep systemctlgrep 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/UbuntuRHEL/Rocky/FedoraAlpine
包管理apt、dpkgdnf/yum、rpmapk
C 库与命令风格GNU coreutilsGNU coreutilsBusyBox(ls、find、sed 参数是精简版)
sudo默认可用默认可用默认没有,需 apk add sudo
服务管理systemctl(容器内常缺)systemctl多用 OpenRC 或 supervisord
防火墙后端ufw 常见firewalld 常见iptables/nftables 裸用
Shell 默认bashbashash(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 为什么不能随手敲。

示例 02:ls 各字段怎么读、目录的进出与搬动在新标签页打开

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-4rwx属主(第三列的 deploy)的权限
第 5-7r--属组(第四列)的权限
第 8-10r--其他人的权限
第 2/5/8 位s 或 Ssetuid(属主位)/ setgid(属组位);大写 S 表示「有标记但没有对应 x」
最后一位t 或 Tsticky bit,常见于 /tmp(drwxrwxrwt)
第 2 列1 / 3硬链接数;目录至少 2(. 和 ..),所以看到 1 说明不是目录
第 6 列1.2K / 4.0K文件字节数;目录显示的是它自己的 inode 表大小,不含内容
第 7 列Oct 4 09:12mtime(内容最后改动时间);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 的三条铁律:

  1. 路径必须先补全再回车。rm -rf $DIR/ 在 $DIR 为空时变成 rm -rf /(新版 coreutils 会拒绝,但 rm -rf /home/deploy/ 不会)。安全写法:rm -rf "${DIR:?变量未设置}/"*。
  2. 不要在 cd 失败的目录里执行。脚本里写 cd /srv/app && rm -rf cache 用 &&,别用 ;——cd 失败时 ; 后面的命令会在当前目录执行。
  3. 保护参数:--preserve-root(默认开启,防 rm -rf /);手生时永远先加 -i。

10. ln —— 软链接与硬链接

Linux 的链接分两种,用法和性质完全不同:

维度软链接(symbolic)硬链接(hard)
创建ln -s /srv/app/releases/v7 currentln 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}.confa.conf b.conf花括号展开发生在通配之前,适合批量建文件
file_{1..5}.tmp5 个名字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 ! 被当成普通文本写进了文件。解法固定三步:

  1. 连按 Esc 两三次(保证回到普通模式);
  2. 输入 :q! 然后 Enter(不想保留改动)或 :wq(保留);
  3. 如果底部显示 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、进程用的不是你以为的用户。这一章先把「读权限位」练熟,再给一套安全的修改顺序。

示例 05:读懂权限位、chmod 两套写法与 Permission denied 排查在新标签页打开

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,每组三位相加:

数字含义常见用途
644rw-r--r--普通文件默认(配置、图片、代码)
755rwxr-xr-x可执行文件、脚本、目录默认
600rw-------私钥、含密码的配置
700rwx------.ssh 目录、只给自己看的目录
664rw-rw-r--团队协作目录里的文件(配合组权限)
775rwxrwxr-x组内共享的目录
777rwxrwxrwx不要用,见下

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   # 递归前先用它跑一遍确认范围

改属主时的三个雷:

  1. chown -R 在目录树里遇到别人正在写的数据库文件会打断服务——先停服务再改。
  2. 递归前先跑一遍确认范围(上面的 find)。
  3. /srv、/var/www 这类由包管理器维护的目录,chown -R root:root 之后可能连 apt 都跑不动。

4. umask —— 新文件的默认权限

umask 是「新建文件时要屏蔽掉的位」。文件最多 666,目录最多 777,然后按位与 ~umask。

umask新文件新目录场景
022644755默认(系统用户、多数发行版)
002664775团队协作组(配合共同属组)
077600700安全敏感环境
027640750常见服务器加固基线

示例 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 cmdsudo -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. 特殊权限位一览

位数字表现作用常见例子
setuid4000属主执行位是 s(rwsr-xr-x)执行时以文件属主身份(root)passwd、ping(旧式)
setgid2000属组执行位是 s(rwxr-sr-x)文件:执行时以属组身份;目录:新建文件继承目录的组团队共享目录 chmod g+s /srv/shared
sticky1000其他人执行位是 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 或 noexecfindmnt -T /srv/app看到 ro 就是磁盘只读挂载(第十四章)
6. 磁盘/inode 满(报的是别的错但很像权限)df -h; df -iNo 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)。

示例 06:find 的条件、动作与 -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 python3PATH 里的可执行文件/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。

示例 07:grep / 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>),把几条命令串成逻辑(&&、||)。搞不清这三个,脚本就只能写成猜谜。

示例 08:重定向、管道与 tee 的行为差异在新标签页打开

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 的字母到底管什么

字母含义单独用
ccreate 创建归档与 x 互斥
xextract 解包—
ttable of contents 列内容不解包先看清单
rrepend 追加(不能压缩)少用
uupdate 只追加更新的文件类似增量备份
f归档file 名,必须紧跟文件名-f - 表示 stdin/stdout
vverbose 打印过程看进度用
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 的每个数字、信号的优先级讲清楚,然后给出杀进程的正确顺序。

示例 10:ps 各列含义、top 读数与 kill 的正确顺序在新标签页打开

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 完全正常
%MEMRSS 占物理内存百分比与 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 zombiezombie 非 0 说明有父进程不回收
第 3 行%Cpu(s): 12.3 us, 3.1 sy, 0.0 ni, 83.9 id, 0.4 wa, 0.0 hi, 0.3 sius 用户、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+ COMMANDVIRT 虚拟、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 发信号(会二次确认)
rrenice 某 PIDG切换到某个 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 与信号 —— 正确顺序(三步,别跳)

信号编号默认行为能不能被捕获用法
HUP1挂断 / 让服务重读配置能kill -HUP 28414(老式重载,nginx 仍支持)
INT2中断(等同 Ctrl-C)能交互场景
QUIT3退出并 dump core(Ctrl-\)能有些服务用它输出栈
TERM15请求终止(默认)能kill 28414 就是它
KILL9立刻杀死,内核回收不能最后手段,见下
STOP19(x86 Linux)强制暂停,不可捕获不能冻结进程做调试;停止状态要 CONT 恢复
CONT18恢复被 STOP 的进程—kill -CONT 28414
USR1/USR210/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 的过滤参数——它们决定了你能不能在三分钟内定位「服务为什么起不来」。

示例 11: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
字段作用常踩的点
Typesimple(默认,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 时挂到哪个 targetmulti-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 时」→ 永远不跑

三个经典坑:

  1. PATH 不同。cron 的环境只有 /usr/bin:/bin,没有你的 ~/.bashrc 里的路径。写绝对路径,或在 crontab 顶部加 PATH=/usr/local/bin:/usr/bin:/bin 和 MAILTO=""。
  2. % 是特殊字符(被当换行/参数分隔)。crontab 里 date +%F 必须写成 date +\%F,或者干脆把命令放进脚本文件(强烈推荐)。
  3. 注释与输出。# 行会被忽略;任务有输出时 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
adduserPerl 友好脚本(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
位置值含义备注
1deploy用户名登录与 chown 用
2x密码占位符真正的哈希在 shadow;这里是空表示无密码
31000UID0=root,系统账户 < 1000,65534=nobody
41000GID(主组号)指向 /etc/group
5Deploy UserGECOS 备注全名/电话,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::
位置值含义
1deploy用户名
2$y$j9T$…密码哈希;前缀 $y$=yescrypt、$6$=SHA-512、$2b$=bcrypt;! 或 * 开头 = 锁定;空 = 无密码可登录
320550最后一次改密距 1970-01-01 的天数(date -d "1970-01-01 +20550 days" 换算)
40改密后至少隔多少天才允许再改(防来回改历史密码)
590密码有效期天数(99999 = 永不过期)
67过期前多少天开始警告
714过期后多少天宽限期,之后账户锁定
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 各管一段,本章按这个顺序给命令。

示例 13:ip/ss/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 -tulnpss -tulnpss 直接从内核接口读,海量连接时快几十倍
全部 TCP 连接netstat -antpss -tanp支持 state 精确过滤
只看 TIME_WAITnetstat -ant | grep TIME_WAITss -tan state time-wait不用 grep,不丢格式
Unix 域套接字netstat -xlpss -xlp输出更完整
路由表route -nip r支持策略路由显示
接口统计netstat -iip -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

后端常见于常用命令
firewalldRHEL/Rocky/Fedorasudo firewall-cmd --permanent --add-port=80/tcp → sudo firewall-cmd --reload;sudo firewall-cmd --list-all
ufwUbuntu/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 找不到大头时还有第三种(被删除但仍在打开的文件)。这一章把磁盘相关命令按「看容量 → 看谁占 → 看设备 → 挂载 → 修文件系统」的顺序排好。

示例 14:df/du 找空间大头与 inode 用满的排查在新标签页打开

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、swapauto 让 mount 猜
4 选项见下逗号分隔,无空格
5 dump0备份工具用,现代基本都填 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」完整排查

步骤命令判断
1df -h块用满 → 进 2;Use% 未满 → 进 3
2du -xh / --max-depth=2 | sort -h | tail找目录大头;日志/docker/临时文件三大来源
2bsudo lsof +L1 | sort -k7 -n | taildu 找不到大头时必查:已删除但句柄未关的文件,df 算它占用;重启对应进程即释放
3df -iinode 满 → 海量小文件,见第 4 节
4findmnt -T /出错的路径落在 ro 的挂载点上 → 是只读问题不是空间问题
5dmesg | tail -30EXT4-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(对挂载分区也不写,但结果不可靠,仅参考)
XFSxfs_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 上命令完全不同。这一章按体系列命令,然后把「三个反向查询」单独讲透——它们才是排障时真正要找的东西。

示例 15:apt / dnf / rpm / dpkg 的查询与安装在新标签页打开

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/UbuntuRHEL 系Alpine
这个文件是谁装进来的dpkg -S /usr/bin/convertrpm -qf /usr/bin/convertapk info -w /usr/bin/convert
这个包装了哪些文件dpkg -L imagemagickrpm -ql imagemagickapk info -L imagemagick
某个命令在哪个包里(还没装)apt-file search bin/convert 或 apt search(或直接装 debscan)dnf provides '*/convert'(最方便,仓库索引里查)apk search -x + 看包描述
谁依赖这个包apt-cache rdepends pkgdnf repoquery --whatrequires --installed pkgapk info -t pkg(依赖树反向)
这个包依赖什么apt-cache depends pkg / dpkg -s pkgdnf repoquery --requires pkgapk 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 才需要)
步骤常见失败处理
./configureno 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)、`,但不拆分单词、不做 globecho "$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(每个新终端窗口)每次开 shellalias、函数、提示符、补全
/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 之后基本不用再手动查
$!最后一个后台进程 PIDcmd & 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 报错正常处理
可用性所有 shbash/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 -euenv 的 -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 与总速查表

机器慢、服务挂、磁盘满、连不上——排障的通用方法是「用一小组命令把每个资源维度各看一眼」,而不是凭猜测乱翻。这一章先给每个资源的观测命令,再给一张「现象 → 先跑哪几条」的映射表,最后收口成「想干一件事 → 用什么命令」的总速查表。

示例 17:现象到命令的排障路径与总速查表在新标签页打开

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)判断内存够不够只看这一列
sharedtmpfs(含 /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(队列深度)
sarsar -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 点发生了什么」的唯一实用工具
vmstatvmstat 1r(运行队列)、b(IO 阻塞数,持续 >0 说明 IO 堵)、si/so(swap 活动)、wa
iotopsudo iotop -o哪个进程在读写(-o 只显示有 IO 的)
pidstat -d 1 -p PID每进程读写 KB/s—
blktrace/biosnoopBCC/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 PIDus 高 = 应用算法/循环;sy 高 = 系统调用风暴;wa 高 = 其实是 IO;load 高但 CPU 空 = D 状态堆
内存吃满 / 被 OOMfree -h → dmesg | grep -i kill → ps -eo pid,user,rss,cmd --sort=-rss | head → cat /proc/meminfo | headavailable 小 + 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/wchanD = 等内核 IO(杀不动,修底层);Z = 僵尸(父进程回收);systemd 拉起 = 用 systemctl stop
时间不对timedatectl → chronyc sources/ntpq -p → hwclock -rNTPSynchronized=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多版本共存时看谁在前
登录不上 SSHssh -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
看磁盘/inodedf -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
验证 DNSgetent 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 这种深度工具。本手册第十七张表就是这张检查单的落地版本。

评论

为保障内容质量,评论需站长审核通过后才会展示;提交后没立刻看到属正常,请勿重复提交。

还没有评论,来抢沙发~