1. 先把命令行当成一门"操作系统方言"来学
很多人第一次接触 Linux 操作命令,是在一台只能敲键盘、没有图形界面的机器上。屏幕上只有一个光标在闪,输入ls回车,出来一串文件名,然后就开始背:cd是切目录,mkdir是建目录,rm是删文件。背了三十条,一上真机还是懵。问题不在于命令太多,而在于没搞明白这些命令背后共用的一套语言规则。Linux 操作命令本质上是一门"方言":它的语法极其简单——命令名 + 选项 + 参数,三件套;词根也少得可怜,大量命令成簇出现(ls/lsof/lsblk,ps/pstree/pgrep),动词和名词的组合方式高度一致。你把规则摸透,看似零散的上百条命令会自己归位。
这篇东西写给两类人:一类是刚上手服务器、被Permission denied和command not found折腾得没脾气的朋友;另一类是用了几年命令,但一直是"复制粘贴型选手"、想系统补齐底层理解的同学。我不会按字母表罗列命令手册,那种内容你man一下就有;我要讲的是这些命令在真实场景里怎么组合、为什么这么组合、哪里会出事。全文会围绕文件操作、文本处理、进程与端口诊断、脚本化四个高频区展开,每个区都配可复现的操作和踩过的坑。看完你至少能做到:拿到一台陌生的机器,知道先敲什么、看什么、怎么定位问题。
1.1 一切皆文件:理解这条铁律之后命令就少背一半
Linux 操作命令之所以能收敛得这么好,根源是"一切皆文件"这个设计。普通文本是文件,目录是文件,硬件设备是文件,内核状态也是文件。带来的直接结果是:读写、查看、复制、权限这套操作可以通用。
举几个具体例子,你就能感觉到这套设计的省力之处:
- 想看机器有几核 CPU,不用装任何工具,
cat /proc/cpuinfo就行,因为 CPU 信息被暴露成了文件。 - 想看内存用量,
cat /proc/meminfo。 - 想临时静音声卡、调节音量,操作的是
/dev/snd/下的设备节点。 - 磁盘本身是
/dev/sda、/dev/nvme0n1,对它做dd就是对整块盘读写。 - 甚至连进程都能当文件看,进程号 1234 的信息在
/proc/1234/下面,cat /proc/1234/cmdline就能看到它的启动命令。
理解这一点之后,很多"专用命令"你会发现只是"通用命令 + 特定路径"的封装。lsblk输出的信息,你在/sys/block/下也能翻到;free读的就是/proc/meminfo。
具体怎么用这条规律来减少记忆负担?我常用的思路是:遇到一个不熟悉的资源,先去/proc和/sys里找对应目录,找到之后用cat、grep、awk直接处理,往往比学一个新命令还快。比如排查某个进程打开了多少文件描述符:
# 进程 1234 当前打开的文件描述符数量 ls /proc/1234/fd | wc -l # 该进程的文件描述符上限 cat /proc/1234/limits | grep "open files"这两行不需要lsof,在极度精简的容器镜像里(很多镜像根本不带lsof)特别好使。我第一次在某个 Alpine 基础镜像里发现lsof: not found的时候,就是靠/proc把问题绕过去的。
注意:
/proc和/sys里的大量文件是只读的虚拟文件,echo进去可能没反应,也可能直接改变内核行为。比如往/proc/sys/下的文件写值属于运行时调参,重启即失效,生产机上不要随手试。
1.2 命令的运行链路:PATH、别名、内建命令与外部命令
敲下ls回车之后发生了什么,这件事搞清楚,能解决掉一大半"为什么我的命令行为和教程不一样"的困惑。
大致流程是这样的:shell 拿到你输入的整行,先做分词和展开(变量替换、通配符展开、命令替换),然后判断这是不是 shell 内建命令(cd、echo、export、alias都是内建的),如果是就直接在 shell 进程内执行;如果不是,就到PATH环境变量列出的目录里从左到右找同名可执行文件,找到第一个就执行,后面的不再找。
由此衍生出几个非常实用的排查动作:
# 看一个命令到底走的是哪个路径 type -a ls # 只看可执行文件路径 which python3 # 看 PATH 的完整内容,注意顺序就是优先级 echo $PATH | tr ':' '\n'type -a比which更值得推荐,因为它会告诉你"这是别名"、"这是内建命令"、"这是 /usr/bin 下的文件",信息更全。我遇到过最典型的一个坑:某台机器上ls被加了一个别名alias ls='ls --color=auto -F',某人写脚本时依赖ls的输出做解析,结果在交互式 shell 里跑得好好的,放进crontab就出错——因为 crontab 的 shell 不加载~/.bashrc,别名不生效,输出格式变了,解析全崩。
再说PATH顺序。默认一般是/usr/local/bin:/usr/bin:/bin这种形态,/usr/local/bin排在前面,这就是为什么自己编译安装的软件会覆盖系统自带的同名命令。如果你装了两个版本的同一工具(比如两个版本的编译环境),which出来的路径和你以为的不一致,八成就是PATH顺序问题。临时解决可以直接用绝对路径调用,长期解决就把PATH调整写进 shell 配置文件。
# 临时把某个目录提到 PATH 最前面,只对当前会话有效 export PATH=/opt/tools/bin:$PATH补一句内建命令的坑:cd、export、alias、umask这些是内建命令,它们改变的是当前 shell 进程的状态。所以你在脚本里写cd /some/dir是有效的,但如果你写bash -c "cd /some/dir",那个cd只影响子进程,父进程的当前目录纹丝不动。这个区别在写脚本时非常关键,很多人第一次写cd之后再执行相对路径命令失败,就是这个原因。
1.3 按场景而不是按字母表来记命令
我的建议是彻底放弃"背命令清单"这件事,改成建立场景索引。真实工作里你遇到的永远是问题,不是命令。下面这张表是我自己桌面便签上的版本,按"我要干什么"来组织,比任何命令大全都好用。
| 我要做的事 | 首选命令 | 备选/补充 |
|---|---|---|
| 找文件在哪 | find | locate(需先建索引) |
| 在文件内容里找字符串 | grep -rn | rg(装了就更快) |
| 看文件尾部新增 | tail -f | tail -F(应对日志轮转) |
| 看磁盘空间够不够 | df -h | du -sh *找大户 |
| 看谁占着 CPU | top | ps aux --sort=-%cpu | head |
| 看端口被谁占了 | ss -lntp | lsof -i:端口 |
| 看进程打开的文件 | lsof -p PID | ls /proc/PID/fd |
| 批量改文件内容 | sed -i | perl -pi -e(正则更强) |
| 按列统计 | awk | cut+sort+uniq -c |
| 定时执行 | crontab -e | systemd timer |
表格里的"备选"列不是摆设。同一个问题在不同机器上最优解不同:容器里可能没有lsof,老系统上可能没有ss只有netstat,最小化安装可能没装locate的数据库。真正熟练的人脑子里存的是一张"问题 → 多条路径"的网,而不是一条命令。
这也解释了一个常见现象:新手看到别人敲出一长串管道命令会觉得对方"记性好"。其实不是,那串命令是现场拼出来的,每一段解决一个小问题,靠管道串起来。第 5 节我们会专门讲管道怎么拼。
2. 文件与目录操作:最容易出事的一块
文件操作是所有 Linux 操作命令里使用频率最高、同时也是破坏力最大的一块。ls敲错顶多没输出,rm敲错可能就没了。这一节我把常用命令里"真正有用的那部分参数"挑出来讲,不讲全量选项,只讲现场用得上和容易翻车的。
2.1 ls、cd、find:把"找东西"这件事做扎实
ls大家都熟,但有三个参数我认为必须形成肌肉记忆:
ls -lh # 人类可读的大小,排查磁盘占用第一眼 ls -la # 显示隐藏文件,很多配置问题就藏在点文件里 ls -lt # 按修改时间倒序,找"最近谁动过" ls -ltr # 反过来,最新的在最下面,配合长输出看更顺ls -lt这个组合我用得极多。某次线上配置突然失效,第一步就是ls -lt /etc/某服务目录/,一眼看到某个配置文件十分钟前被改过,问题范围瞬间从"玄学"收窄到"某次变更"。
cd本身没什么好说的,但有两个小技巧值得养成习惯:
cd - # 回到上一个目录,两个目录之间来回跳 cd # 不带参数,直接回当前用户的家目录find是文件操作里信息量最大的一个命令,也最容易被写成"抄来的一段看不懂的咒语"。我把它的结构拆开:find 起点 条件 动作。三部分各管一件事,读的时候按块读。
# 在 /var/log 下找 7 天内修改过、大于 100M 的 .log 文件 find /var/log -type f -name "*.log" -mtime -7 -size +100M # 找出所有权限是 777 的文件(安全检查常用) find /data -type f -perm 0777 # 找出 30 天前的文件并删除(删除前务必先只打印验证) find /data/tmp -type f -mtime +30 -print find /data/tmp -type f -mtime +30 -delete这里必须强调一条铁律:任何带-delete或-exec rm的find,第一次执行时一定要把动作换成-print先看一遍结果。我见过不止一次有人把-mtime +30写成-mtime -30,符号一颠倒,删的是最近 30 天的文件,性质完全反了。-print那一步只花你三秒钟。
时间参数这块也值得单独说清楚,因为它是误删重灾区:
| 参数 | 含义 | 记忆方法 |
|---|---|---|
-mtime -7 | 7 天以内修改过 | 负号像"最近" |
-mtime +7 | 7 天以前修改过 | 正号像"以前" |
-mmin -30 | 30 分钟以内 | m 换成分钟 |
-newer file.txt | 比 file.txt 更新的 | 拿一个基准文件对比 |
-newer这个参数实际很好用。比如你怀疑某次发布之后系统状态发生了变化,拿发布时留下的一个时间戳文件当基准,find /etc -newer /tmp/deploy_stamp,就能把所有在那之后被改动的配置列出来。
2.2 rm、cp、mv 的三条保命习惯
rm的危险性不需要解释,但光说"小心点"没用,得有具体机制。我自己固定执行三条:
第一条:rm -rf之前先ls同一个路径。这不是形式主义。路径拼错是最高频的误删原因,/data/app/logs少打一个s就变成/data/app/log,可能是个完全不同的东西。先ls一遍,眼睛确认输出的是你想删的东西,再按上方向键改命令。
第二条:变量一定要加引号,且用${VAR}形式。这条在脚本里是硬性的:
# 危险写法 rm -rf $TARGET_DIR/ # 相对安全的写法 rm -rf "${TARGET_DIR:?}/"${VAR:?}的含义是:如果变量为空或未定义,直接报错退出,不执行后面的命令。少了这个保护,一旦变量因为某种原因没赋值,rm -rf "$TARGET_DIR/"会变成rm -rf /,后果不用多说。这是 shell 里少数几个真正能"救命"的语法糖。
第三条:重要的删除操作走"移动而非删除"。在生产机上我的习惯是建一个/data/.trash/目录,删除时用mv移进去,定期清理。多占一点磁盘,换回来的是后悔药。
# 一个极简的"回收站"函数,加进 ~/.bashrc trash() { mkdir -p /data/.trash && mv -v "$@" "/data/.trash/$(date +%s)_$(basename "$1")"; }cp和mv的坑主要集中在两点。一是覆盖行为:cp默认直接覆盖不提示,mv也一样。想有提示就加-i,或者在~/.bashrc里加alias cp='cp -i'、alias mv='mv -i'、alias rm='rm -i'。二是跨文件系统移动:同一个文件系统内mv是改目录项,瞬间完成;跨文件系统时mv实际上是"复制 + 删除",大文件会很慢,中途中断可能留下半个文件。所以几百 G 的数据迁移,我不会用mv,而是rsync -a --remove-source-files,可断点续传、可校验、可限速。
rsync -av --progress --remove-source-files /data/old/ /data/new/-a保留了权限、时间戳、软链接等属性,--remove-source-files在传输成功后才删源文件,比mv稳得多。
2.3 chmod、chown、umask:权限数字是怎么算出来的
权限这块,很多人是靠查表活着的:777 是啥、755 是啥、644 是啥。查表当然能干活,但一旦遇到umask或者"为什么我新建的文件是 664 不是 644"这种问题就卡住了。其实规则非常简单。
权限分三组:属主(u)、属组(g)、其他(o)。每组三个位:读(r=4)、写(w=2)、执行(x=1)。三个位相加就是一个八进制数字。所以:
7= 4+2+1 = rwx6= 4+2 = rw-5= 4+1 = r-x4= r--0= ---
chmod 755 file就是属主 rwx、属组 r-x、其他 r-x。这个算法熟了你就不需要查表了。
chmod 644 config.yml # 普通配置文件:属主可写,其他人只读 chmod 755 run.sh # 脚本:属主可执行,其他人可读可执行 chmod 600 id_key # 私钥类文件:只有属主能读写 chmod 700 /data/private # 私有目录:其他人连进都进不去chown改属主和属组,格式chown 用户:组 文件,配合-R递归:
chown -R www-data:www-data /var/www/site有个细节容易忽略:chown -R递归时,如果目录下有软链接,默认是不跟随的(改的是链接本身)。要跟随就用-h的反面——注意 GNU 版本下chown默认行为是不跟随软链接,而chmod -R遇到软链接也不跟随。这个在部署时踩过一次:一批软链接指向的文件权限没被改到,服务启动时读不了,排查了半天才发现是递归没跟过去。
umask是"默认权限的掩码",这个概念最容易绕晕。规则是:新建文件的默认权限 = 666 - umask;新建目录的默认权限 = 777 - umask。
umask # 查看当前值,通常是 022 或 002 umask 022 # 设置:文件 644,目录 755 umask 077 # 设置:文件 600,目录 700(私有,最严格)为什么文件基数是 666 而不是 777?因为新建文件默认不给执行位——防止你写了个文本文件,结果因为权限配置被当程序执行了。这个设计是刻意的。
有了这个公式,"为什么我新建的文件是 664"就能自己算出来:umask是002(某些发行版对用户组内的成员更宽松),666 - 002 = 664,组内成员也能写。这个值在多人协作的共享目录里是合适的,但在生产环境里往往过于宽松,值得检查一遍。
3. 文本处理三剑客:日志排查的主力
服务器上没有 IDE,没有图形化的日志查看器,你能依赖的就是grep、sed、awk加上sort、uniq、wc这几个小工具。这一节围绕"一次完整的线上问题排查"来讲,而不是逐个讲语法。
3.1 grep:先缩小范围,再谈分析
排查日志的第一原则是:永远不要直接cat一个几百兆的日志。先缩小范围。grep就是干这个的。
grep -c "ERROR" app.log # 先数一下有多少条 grep -n "ERROR" app.log | head -20 # 带行号看前 20 条 grep -rn "NullPointer" /var/log/app/ # 递归搜目录,带行号 grep -v "DEBUG" app.log # 反向匹配:排除噪音 grep -i "timeout" app.log # 忽略大小写 grep -A 5 -B 5 "ERROR" app.log # 前后各带 5 行上下文-A/-B/-C这三个参数是排查日志的关键。异常栈是一段一段的,只匹配到Exception那一行等于什么都没看到,必须把上下文带出来。-C 10表示前后各 10 行,一般够用。
几个实战组合值得记住:
# 找出所有非 200 状态码的请求,统计各状态码分布 grep -oE "status=[0-9]{3}" access.log | sort | uniq -c | sort -rn # 找出某个时间窗口内的错误 grep "2024-05-20 1[0-2]:" app.log | grep "ERROR" # 排除健康检查和使用内部探针的请求,看真实流量 grep -v -E "(health|ready|metrics)" access.log | grep " 500 "第二条里1[0-2]是正则的字符区间,表示 10 点、11 点、12 点三个小时。这个技巧在处理按小时切分的日志时特别顺手。
注意:正则里的
[在 shell 中有通配符含义,虽然放在引号里就安全,但更稳妥的习惯是所有正则都加单引号。grep '[0-9]' file永远比grep [0-9] file安全,后者在当前目录恰好有名为9的文件时会展开成文件名。
还有一个实践中的取舍:如果机器上装了ripgrep(命令名rg),在大量文件中递归搜索时优先用它,速度快一个数量级,而且默认遵守.gitignore。但要注意它的默认行为差异——rg默认跳过隐藏文件和二进制文件的匹配方式与grep -r不同,脚本里依赖固定输出时还是用grep更可控。
3.2 sed 与 awk 的分工边界
这两个工具经常被混用,其实分工很清楚:
sed是流编辑器,擅长"按行做替换、删除、插入",输入输出都是文本流,不擅长结构化计算。awk是文本处理语言,擅长"按列取字段、做条件判断、求和计数"。
判断该用哪个,看你的动作是不是跟"列"有关。只改字符串内容,用sed;要按字段统计,用awk。
sed最常用的就三类操作:
# 1. 替换:把文件里的 old 换成 new,加 g 表示每行全部替换 sed -i 's/old/new/g' config.txt # 2. 打印指定行范围 sed -n '100,200p' app.log # 3. 删除包含某关键字的行 sed -i '/^#/d' config.txt # 删掉注释行sed -i的坑必须说:它直接修改原文件,没有备份。GNU sed 支持-i.bak先备份再改,我强烈建议养成这个习惯:
sed -i.bak 's/8080/9090/g' app.conf一个真实翻车案例:有人写sed -i 's/old/new/g' *,当前目录里有个软链接指向生产配置,sed跟随软链接把生产文件改了。所以sed -i通配全目录这种操作,先ls *看一遍目录内容,或者加--follow-symlinks的反面(默认不跟随,但不同实现有差异),别赌。
替换内容里含/的时候,s/old/new/会语法错乱,比如替换路径。解法是换分隔符,sed允许用任意字符做分隔:
sed -i 's#/usr/local/old#/usr/local/new#g' app.confawk的核心是awk '模式 { 动作 }',默认按空白分列,$1是第一列,$NF是最后一列。
# 取第 1 列和第 3 列 awk '{print $1, $3}' data.txt # 按指定分隔符(冒号)取第 7 列,去重 awk -F: '{print $7}' /etc/passwd | sort -u # 条件过滤 + 求和:统计 500 错误的总字节数 awk '$9 == 500 {sum += $10} END {print sum}' access.log # 按某列分组统计 awk '{count[$1]++} END {for (k in count) print k, count[k]}' access.log-F:这个写法要熟,因为/etc/passwd、/etc/group、以及大量日志格式都是冒号或特定字符分隔的。$NF表示最后一列,$(NF-1)是倒数第二列——处理字段数不固定的行时特别好用。
3.3 一次完整的日志排查链路实录
光讲单个命令没意义,我把一次真实的排查过程还原一遍。场景是某项目接口在凌晨出现间歇性超时,需要从日志定位原因。
第一步:确定问题时间窗口。先看错误总数和分布,判断是持续性还是尖峰式。
grep -c "timeout" app.log grep "timeout" app.log | awk '{print substr($1,1,13)}' | sort | uniq -c这里substr($1,1,13)取时间戳前 13 位(形如2024-05-20 10),把秒级时间聚合成小时,uniq -c出来的一列数字就是每小时的超时数。尖峰在哪一小时,一目了然。
第二步:看尖峰时段的具体请求。缩小到那一小时,把耗时的和超时的都捞出来。
grep "2024-05-20 03:" app.log | grep -E "(timeout|cost=[0-9]{4,})" | head -50cost=[0-9]{4,}匹配耗时四位数以上(毫秒计即 1 秒以上)的请求。-E开启扩展正则,|表示或。
第三步:找出慢请求的共同特征。假设日志格式里第 7 列是接口路径。
grep "2024-05-20 03:" app.log | awk '$10 > 1000 {print $7}' | sort | uniq -c | sort -rn | head这一条把"3 点这一小时内、耗时字段大于 1000 的请求"的接口路径提取出来,按出现次数倒排。前三名就是重点怀疑对象。
第四步:看这几个接口当时的调用量和并发。
grep "2024-05-20 03:" app.log | grep "/api/order/query" | wc -l如果这个接口在这一小时的调用量是平时的十倍,基本可以确定是流量突增;如果调用量正常但慢请求集中,那更像外部依赖(数据库、下游服务)在抖动。往下查就是看同机其他日志,或者用第 4 节的进程诊断命令看系统负载。
这套流程的价值在于每一步都在收缩范围:全量日志 → 一小时 → 一类错误 → 一个接口 → 一个数量级判断。全程没有cat整个文件,都是在流上操作,几百兆的日志也是秒级响应。
心得:日志字段顺序一定要先搞清楚再写
awk。我踩过的坑是把耗时字段的列号记错,白白分析了半小时才发现统计的是一列无关的数字。先用head -3 app.log加awk '{for(i=1;i<=NF;i++) print i, $i}'把每列标出来,比凭记忆靠谱。
4. 进程、资源与端口:现场诊断
文件操作是"日常",进程和端口诊断才是"救火"。这类命令的特点是有明确的检查顺序,乱敲会浪费时间。我给自己定的顺序是:先看整体负载,再看具体进程,最后看进程在等什么。
4.1 ps、top、pidstat:从全局到单个进程
top是入口,但它的默认交互体验对新手不友好。先用几个一次性命令把宏观情况摸清楚。
uptime # 输出:10:30:00 up 30 days, 2:10, 3 users, load average: 1.20, 0.85, 0.60load average三个数字分别是 1 分钟、5 分钟、15 分钟的平均负载。判断标准是拿它和 CPU 核数比:4 核机器负载 4 大约是满载,长期超过 4 就说明有排队。看趋势比看绝对值重要——从右往左看,如果是递减的(15 分钟 > 5 分钟 > 1 分钟),说明负载在回落;如果是递增的,说明问题正在发生。
# 按 CPU 倒序取前 10 ps aux --sort=-%cpu | head -11 # 按内存倒序 ps aux --sort=-%rss | head -11 # 看进程树,理清父子关系 ps -ef --forest | grep -A 5 "某服务名"--sort这个参数我强烈推荐,比ps aux | sort -k3 -rn更直观,字段名直接写,不用记列号。ps aux输出里%CPU是第 3 列、%MEM是第 4 列、RSS(实际物理内存)是第 6 列,但不同实现有差异,用--sort一劳永逸。
进程树在实际排查中价值很高。如果某个主进程下面挂了 50 个子进程在抢占 CPU,说明是主进程"放得太开"。
pidstat不在最小化安装里,需要装sysstat包,但它的价值完全值这个麻烦——它给的是时间区间内的增量,,而不是top那种"采样瞬间"。这两个信息的含义区别很大。
# 每 2 秒采样一次,共 5 次,看某个进程的 CPU 和内存 pidstat -p 1234 2 5 # 看所有进程的磁盘 IO,找出读写大户 pidstat -d 2 5top里显示的%CPU是自上而下的采样估算,某些短时突发的高占用会被平滑掉;pidstat在区间内累加,更能反映真实消耗。有一次排查"某个进程看起来 CPU 不高,但机器整体负载很高",就是靠pidstat -d发现它在疯狂做磁盘 IO——CPU 全在iowait上,进程本身的用户态 CPU 确实不高。
这也引出一个基本判断:看 CPU 高之前,先看是真算还是干等。top里%wa(wait)和%id(idle)两个值很关键。如果%wa持续很高,说明 CPU 在等磁盘;如果%id高但负载也高,那可能是大量进程在等锁或者 IO。
4.2 lsof 与端口占用排查
"端口被占用"是开发环境最高频的问题之一。标准解法是三步:
# 1. 看端口被谁占了 ss -lntp | grep 8080 # 2. 如果输出里有 PID,直接看这个进程 ps -fp 1234 # 3. 确认后决定是杀还是换端口 kill -15 1234 # 先礼 kill -9 1234 # 后兵ss参数解释:-l只看监听状态,-n不做域名解析(快),-tTCP,-p显示进程(需要 root 权限才能看到别人的进程)。老系统上可能只有netstat,对应的写法是netstat -lntp。
# 看所有已建立的 TCP 连接及其状态统计 ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn这一条的输出能直接看出连接状态分布。ESTAB是正常连接数量,TIME-WAIT是主动关闭方留下的,CLOSE-WAIT大量堆积则说明有一方没正确处理关闭——这通常是应用层代码的问题,不是网络问题。
lsof的信息维度更宽,它能列出进程打开的一切:文件、目录、网络连接、设备。
lsof -p 1234 # 这个进程打开了啥 lsof -i:8080 # 谁在用 8080 端口 lsof +D /data/uploads # 哪些进程在用这个目录(递归,很慢) lsof /data/app.log # 谁占着这个文件最后一个用法解决过一个经典问题:日志文件被删了但磁盘空间没释放。原因是某个进程还持有文件句柄,df显示磁盘满,du加起来却对不上。用lsof | grep deleted能找出这类"幽灵文件",处理方法是重启对应进程(或者用> /proc/PID/fd/N清空)。
4.3 磁盘与内存:df、du、free 的正确打开方式
磁盘排查有一条经验公式:df看总量,du找大户,lsof抓幽灵。
df -h # 各分区使用率 du -sh /data/* 2>/dev/null | sort -rh | head -10 # /data 下前 10 大目录 du -ah /data/logs | sort -rh | head -20 # 深入到文件级du -sh加2>/dev/null是为了屏蔽权限不足的报错噪音,很有必要——不然你在根目录跑一次,屏幕会被刷满。
free有个新手最常问的问题:"为什么 available 和 free 差那么多?"因为 Linux 会把空闲内存拿去做缓存(buff/cache),这是设计如此,不是内存泄漏。真正该看的是available那一列——它表示"还能分给新应用的内存",是包含可回收缓存的。free接近 0 但available充足是完全正常的。
free -h # 重点看 available 这一列如果真要清缓存(一般不建议,除了压测场景需要冷启动环境):
sync && echo 3 > /proc/sys/vm/drop_caches这条命令会丢弃页缓存、目录项和 inode 缓存,执行后系统短期内会变慢(要重新从磁盘读)。生产机上不要为了"让内存数字好看"去执行,缓存本来就是花内存换性能。
5. 把命令串起来:管道、重定向与脚本
单个命令只是零件,真正的生产力来自组合。这一节讲管道和重定向里那些"看着简单但细节很多"的地方,以及怎么把一串常用命令固化成一个能复用的脚本。
5.1 管道与重定向:几个必须精确的细节
管道|把前一个命令的标准输出接到后一个命令的标准输入。重定向>、>>、2>、&>、<则是把三个标准流(stdin=0,stdout=1,stderr=2)接到文件上。
最容易出错的是>和>>的区别,以及 stderr 的处理:
cmd > out.log 2>&1 # 标准输出和错误都写进 out.log(覆盖) cmd >> out.log 2>&1 # 追加 cmd &> out.log # bash 的简写,等价于上面第一行 cmd 2>/dev/null # 丢弃错误输出2>&1的顺序有讲究。cmd > out.log 2>&1是对的:先把 stdout 指向文件,再让 stderr 复制 stdout 的指向。如果写成cmd 2>&1 > out.log,stderr 会复制"当时"的 stdout(终端),然后再把 stdout 指向文件——结果就是错误还是打在屏幕上。这个顺序问题我在别人的脚本里见过太多次。
还有一个非常实用的重定向技巧:同时输出到屏幕和文件。
cmd 2>&1 | tee out.log cmd 2>&1 | tee -a out.log # 追加模式tee命令得名于水管的三通接头,非常形象。做耗时任务时用它,既能看到实时进度,又留下完整记录。
>有个隐蔽的危险:它会在命令执行之前就清空目标文件。所以cat file > file会得到空文件,因为打开的一瞬间原内容就没了。安全写法是用临时文件中转,或者用sponge(moreutils 包提供):
cat file | sponge file管道还有个默认行为值得知道:管道中的每个命令都在子 shell 里运行。这意味着在管道里改变量,出了管道就没了:
count=0 echo "a b c" | while read -r x; do count=$((count+1)); done echo $count # 输出 0,不是 3这是新手写脚本最经典的坑之一。解法是避免在管道里累积变量,或者用进程替换(while ... done < <(command)),或者让整个 pipe 在同一个 shell 里执行。
5.2 shell 脚本骨架与 set 三件套
把命令固化下来的第一步是写脚本。我的脚本开头固定这几行:
#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t'这四行不是仪式,每一行都在挡一类事故:
set -e:任何命令返回非零就立即退出。不加这个,脚本中间失败了会继续往下跑,可能造成更严重的后果。set -u:使用未定义变量时报错退出。这条是防rm -rf "$DIR/"里DIR没赋值那种灾难的。set -o pipefail:管道中任一环节失败,整个管道就返回失败。不加的话,管道的退出码只看最后一个命令,grep xxx nofile | wc -l会返回 0,因为你只判断了wc。IFS=$'\n\t':把字段分隔符设为换行和 Tab,避免文件名带空格时被拆成多个词。
最后一条在批处理文件时是必须的:
# 危险:文件名有空格就废 for f in $(ls *.log); do echo "$f"; done # 安全:用 glob 直接展开 for f in *.log; do echo "$f"; done # 更安全:含子目录 while IFS= read -r -d '' f; do echo "$f"; done < <(find . -name "*.log" -print0)-print0配合read -d ''是处理任意文件名(包括含空格、换行)的标准姿势。这个组合看着别扭,但它是唯一不会出错的方式。
一个完整的实用脚本骨架长这样:
#!/usr/bin/env bash set -euo pipefail LOG_DIR="/data/logs" RETENTION_DAYS=7 usage() { echo "用法: $0 [-d 日志目录] [-n 保留天数]" exit 1 } while getopts "d:n:h" opt; do case "$opt" in d) LOG_DIR="$OPTARG" ;; n) RETENTION_DAYS="$OPTARG" ;; *) usage ;; esac done [[ -d "$LOG_DIR" ]] || { echo "目录不存在: $LOG_DIR" >&2; exit 1; } echo "清理 $LOG_DIR 下 $RETENTION_DAYS 天前的日志..." find "$LOG_DIR" -type f -name "*.log" -mtime "+$RETENTION_DAYS" -print -delete注意-print -delete的顺序:先打印再删除,删除的每一条都会显示出来,事后有据可查。
5.3 crontab:定时任务的三个隐形坑
crontab -e编辑当前用户的定时任务,格式是五段加命令:
分 时 日 月 周 命令 0 3 * * * /opt/scripts/clean.sh >> /data/logs/clean.log 2>&1第一个坑:环境变量不继承。crontab执行时用的是极简环境,PATH通常只有/usr/bin:/bin,你在~/.bashrc里配的所有东西都不生效。所以脚本里最好全用绝对路径,或在 crontab 顶部显式定义:
PATH=/usr/local/bin:/usr/bin:/bin SHELL=/bin/bash第二个坑:工作目录不确定。默认在用户家目录,不是脚本所在目录。脚本里依赖相对路径必挂。所以脚本里要么用绝对路径,要么开头先cd到目标目录。
第三个坑:输出没地方去。cron 执行失败会尝试发邮件,但大多数机器没配邮件服务,报错就静默消失了。所以每条任务都加上>> 日志 2>&1,出问题能查。
还有一点容易忽略:%在 crontab 里是特殊字符(表示换行),如果命令里有date +%Y%m%d,必须转义成\%:
0 3 * * * /opt/scripts/backup.sh "$(date +\%Y\%m\%d)" >> /data/logs/bak.log 2>&1这个坑不踩一次很难记住。想绕开它,可以把这个逻辑挪进脚本内部,crontab 里只写0 3 * * * /opt/scripts/backup.sh,干净得多。
6. 常见问题速查与踩坑清单
这一节是纯粹的经验汇总,都是我在实际环境里反复遇到的东西。
6.1 报错速查表
| 报错信息 | 常见原因 | 排查动作 |
|---|---|---|
command not found | 没装 / 不在 PATH / 拼错 | type -a 命令,echo $PATH |
Permission denied | 缺执行位 / 缺读权限 / 目录无 x 位 | ls -l看权限,检查每级目录 |
No such file or directory(文件明明存在) | 脚本的 #! 解释器路径错了 | head -1 script.sh,file script.sh |
Text file busy | 文件正在被运行 | lsof 文件,先停进程再覆盖 |
Argument list too long | 通配符展开的文件太多 | 改find ... -exec或xargs |
Too many open files | 文件描述符上限到了 | ulimit -n,cat /proc/PID/limits |
No space left on device | 磁盘满 / inode 满 | df -h,df -i,lsof | grep deleted |
Cannot allocate memory | 内存或 overcommit 限制 | free -h,dmesg | tail |
Address already in use | 端口被占 | ss -lntp | grep 端口 |
Bad interpreter | 脚本在 Windows 下编辑过,行尾是 CRLF | dos2unix或sed -i 's/\r//' |
倒数第二条的df -i值得单独提一句。磁盘空间没满但写不进去,很可能是 inode 用完了——大量小文件场景(邮件队列、缓存目录、session 文件)特别容易碰到。df -h显示有空间,df -i显示 100%,那就得去清小文件,删大文件没用。
倒数第一条也是高频问题:脚本在本地编辑器写好、传到服务器上就跑不了,报bad interpreter。原因是行尾符是 CRLF 而不是 LF。用file script.sh一看,输出里带with CRLF line terminators就确诊了。
6.2 我自己的几条铁律
第一,破坏性命令先降级执行。rm换成ls,find -delete换成-print,sed -i换成不带-i先看输出。三秒钟的成本,换的是不可逆操作的安全边际。这个习惯我保持了七八年,中间救回来至少四五次。
第二,执行前先确认自己在哪、以谁的身份。有一组命令我几乎形成了条件反射:
whoami && pwd && hostname看起来啰嗦,但多开几个终端窗口的时候,你真的会搞不清这个窗口连的是测试机还是生产机。尤其是用同一套终端配色的时候。我现在的做法是给不同环境的 shell 提示符配不同颜色,PS1里带上主机名,视觉上就不会混。
第三,任何"一次改一批"的操作,先拿一个样本试。批量替换配置文件、批量改权限、批量重命名,先对单个文件执行,验证输出正确,再上全量。
# 先对一个文件试 sed 's/old/new/g' config-01.conf | diff config-01.conf - # 确认 diff 输出符合预期,再批量 find . -name "config-*.conf" -exec sed -i.bak 's/old/new/g' {} +| diff 原文件 -这个用法很好用,-表示标准输入,能直接对比"改完的样子"和"现在的样子",比人眼扫一遍可靠得多。
第四,多用man和--help,但别指望从头读到尾。man页面的正确用法是搜索:进去之后按/输关键字,n跳下一个。比如查find的时间参数,直接man find然后/mtime,比在网上搜一堆互相抄的文章快且准。
第五,命令的历史记录是你的资产。Ctrl+R反向搜索历史命令,配合history | grep 关键字,比重新敲一遍快得多。我还会定期把一些复杂的组合命令记到一个~/notes/cmd.md里,按场景分类。真正高频的那二三十条,用着用着就刻进肌肉记忆了,剩下的查笔记就行——没必要追求"全部记住",那是背单词的思路,不是用工具的思路。
最后分享一个我最近才养成的小习惯:任何在终端里一次敲超过三行的复杂操作,我都会先在/tmp下建个.sh文件写清楚再执行。一是能改,二是留下记录,三是下次直接复用。终端里一口气敲完看似帅,但出了错要整段重来,还不容易复盘。慢一点,稳一点,实际上更快。