Linux操作基础:从命令行逻辑到故障排查的工程实践
很多人第一次装完系统、打开那个黑色终端窗口的时候光标一直在闪人却卡在那儿——不知道敲什么也不太敢乱敲生怕一个回车把系统搞坏。我带过几批新人这个场景几乎每次都重演有人把命令当咒语死记过两天忘光有人照着教程复制粘贴一粘就报错报错了也看不出哪一步偏了。这篇讲Linux 操作基础的内容我打算换一种组织方式不按命令字典那样从 A 排到 Z而是按你真正会遇到的顺序来铺。先把命令行的运行逻辑讲透再讲目录结构然后是文件增删改查、权限模型、进程与磁盘排查、管道和文本处理最后拿一条完整的排查链路收尾。命令行是这套体系里绕不开的核心技能但它的门槛其实不在命令数量上而在知道自己在哪个位置、动的是什么东西。无论你是刚接触服务器的新手还是用了几年但一直靠搜索解决问题的人下面这些内容应该都能对上一部分。1. 终端不是黑窗口而是带上下文的执行器1.1 一条命令的三段式结构大部分人学命令的方式是背词条比如ls 是列目录、cd 是切目录这种方式在前期有效但很快就会碰到天花板——一旦参数变多、命令变长就完全看不懂了。真正省力的方式是先看清所有命令的共同骨架命令名 选项 操作对象三者用空格隔开。比如ls -lh /var/logls是命令名-lh是选项-l长格式、-h人类可读单位/var/log是操作对象。理解了这三段你看到任何一条陌生命令都能先拆开再去查具体某一段是什么意思。选项有两种写法短选项用单个减号加一个字母可以合并写-l -h等价于-lh长选项用双减号加一个单词比如--human-readable语义更清楚但打字更长。我个人的习惯是日常手敲用短选项图快写进脚本或文档给别人看的时候用长选项可读性优先。这个习惯看起来很小但脚本维护成本差距很大——半年后回来看--max-depth1和-d 1前者几乎不需要回忆。还有一点值得早点建立命令的返回值。每条命令执行完都会返回一个 0 到 255 的整数0 表示成功非 0 表示失败存在内置变量$?里。这个机制是后面写脚本、做条件判断的基础也是排查问题时最直接的信号来源。你可以随手敲一条命令然后立刻echo $?看一眼会有种原来它一直在告诉我结果的恍然感。ls /etc /dev/null echo $? # 0 ls /not-exist /dev/null echo $? # 2提示把不需要的输出丢到/dev/null是个好习惯它是个黑洞文件写进去的东西直接消失不会占用磁盘。1.2 没有输出往往才是正确结果新手最容易误判的一点是命令跑完什么都没打印就以为失败了。但很多命令的设计哲学恰恰相反——成功时保持沉默失败时才说话。比如cp a.txt b.txt复制成功就是一片安静mkdir /data/logs建好了也不会告诉你已创建。反过来说如果它输出了什么通常意味着有情况需要你关注。这个设计在批量操作时特别有价值。设想你要创建一百个目录如果每个都打印一行创建成功屏幕会被刷满真正出问题那一行反而被淹没了。沉默式成功让异常自动浮出水面这是命令行工具几十年演化下来的共识做法不是偷懒。1.3 我给自己定的四条操作纪律命令行最让人心慌的地方是不可见性你敲下去的东西和它造成的结果之间没有图形界面那种即时反馈。我的做法是用四条纪律来对冲这种不确定第一动手前先确认当前在哪。pwd这个命令我一天要敲几十次看着多余但rm -rf *在错误的目录下执行后果没法挽回。第二破坏性命令先加-i。rm、mv、cp都支持-i交互确认覆盖前会问你一句。批量操作时嫌烦可以不加但手敲单条命令时加上几乎零成本。第三路径写全别依赖默认。尤其是删除类操作rm -rf /data/tmp/cache/*和rm -rf *之间差的不是几个字符而是整个职业生涯的运气。第四重要操作先ls一遍再动手。想删/data/tmp下所有内容先ls /data/tmp看看里面是什么这一步花两秒能挡掉绝大多数误删。这几条听起来像废话但它们都是从别人的事故里归纳出来的。技术能力可以慢慢涨操作习惯必须一开始就立住因为习惯这东西改起来比学新命令难多了。2. 目录结构认不清后面每一步都在猜2.1 根目录下那些文件夹各自管什么Linux 的文件系统和另一类系统有个根本区别它是单根树。所有东西都挂在/底下没有 C 盘 D 盘的概念插进来的硬盘、U 盘也是挂到树上的某个点挂载点而不是变成一个新的根。这个模型第一次理解起来会有点别扭但一旦接受路径思维就统一了——任何文件都有一个从/开始的完整路径不存在另一个盘的路径。根下面那些固定名称的目录是很多人忽视但极其重要的一层知识。不知道/etc放什么你配置服务时就会到处乱翻不知道/var放什么磁盘满了就找不到源头。下面这张表是我给新人整理的最小认知集目录管什么常见误用/etc系统和服务的配置文件以为它是可执行文件目录/var会持续变化的数据日志、缓存、数据库文件把大文件往这儿扔最后撑爆/usr已安装的软件和库/usr/bin是主要命令区手动往/usr/bin拷二进制/home普通用户的家目录用 root 在里面乱建文件/tmp临时文件可能被系统定期清理把重要数据放这儿当长期存储/opt第三方软件的自装目录和/usr/local混用找不到装哪了/dev设备文件直接对设备文件做写操作/proc内核和进程的运行时信息虚拟的以为里面是真实磁盘文件表里最值得多说一句的是/var和/tmp。/var/log是日志堆积的重灾区服务跑上几个月日志能把分区吃满这是运维里最常见的故障之一。而/tmp虽然叫临时目录很多系统会配置自动清理策略清理周期还不一定告诉你把东西放那儿当仓库迟早出问题。2.2 绝对路径和相对路径选哪个不是随意的绝对路径从/写起比如/etc/ssh/sshd_config位置唯一在哪儿敲都指向同一个文件。相对路径从当前目录算起./config、../logs、logs/app.log都属于这一类它的含义随你的位置变化。什么时候该用哪个我的经验是按场景分脚本里一律用绝对路径交互操作可以用相对路径。脚本运行时的当前目录是不可控的写定时任务的时候尤其明显——任务被调度器拉起来时工作目录往往不是你想象的那个相对路径会直接失效。而交互操作时用相对路径省事cd ..、cd -、vi conf/app.yaml打起来顺手。路径里还有几个高频符号值得记牢。.表示当前目录..表示上一级~表示当前用户的家目录-在cd命令里表示上一次所在的目录。cd -这个用法我强烈推荐养成习惯在两个目录之间来回切的时候比敲完整路径快太多。2.3 一个让我丢过东西的 cd 习惯说个真实的教训。早年我写部署脚本习惯性地在脚本开头写cd /opt/app但那台机器上路径其实是/opt/appscd失败了脚本没有停继续往下执行后面的rm -rf ./dist/*。因为当前目录还停在启动脚本时所在的目录那条删除命令删掉的是另一个位置的内容。问题的本质是cd失败不会中断脚本。默认情况下 shell 不检查每条命令的返回值一条失败了继续跑下一条。修复方式有两种一是脚本开头加set -e让任何一条命令失败就整体退出二是写成cd /opt/apps || exit 1显式处理失败。我现在的习惯是两个都加双保险因为set -e在某些管道和子 shell 场景下有它管不到的角落。#!/usr/bin/env bash set -euo pipefail cd /opt/apps || { echo 目录不存在退出; exit 1; }set -u的作用是遇到未定义变量就报错退出set -o pipefail让管道中任意一环失败都被识别为失败。这三个开关基本是我所有脚本的标配开头写一次省很多事。3. 增删改查把 rm 当炸药对待3.1 ls -l 的输出怎么一行一行读ls谁都会用但ls -l那一行输出里的每个字段能完整读下来的人并不多。而这一行信息量极大读懂了它文件类型、权限、属主、大小、时间全都一目了然。ls -l /var/log # -rw-r----- 1 root adm 1234567 Mar 12 09:20 syslog从左往右拆第一个字符-是文件类型-普通文件、d目录、l软链接、c字符设备、b块设备、s套接字、p命名管道。紧接着九位是权限三位一组分别对应属主、属组、其他人。再往后是硬链接数、属主、属组、字节大小、修改时间、文件名。前面表里那个1是硬链接计数普通文件通常是 1目录则至少是 2.和..各占一个。-h参数把字节数换成 K/M/G实际使用中基本必加1234567和1.2M相比后者一眼就知道要不要处理。-t按修改时间排序-S按大小排序-r反转顺序。这几个组合起来找最近改过的大文件就很方便ls -lhtr /var/log | tail -53.2 cp、mv、rm 的风险控制这三个命令的共同点是默认不确认、静默覆盖。cp a b如果b已存在直接盖掉不留备份mv同理rm更是连回收站都没有。理解这一点之后防护手段就很清楚了。cp有两个参数特别好用。-a是归档模式等价于-dR --preserveall保留权限、属主、时间戳、软链接本身做整目录备份时应该默认用它否则拷出来的东西权限全乱。-n是不覆盖已存在的文件-u是只在源文件更新时才覆盖这两个在做增量同步的时候能派上用场避免把目标端改过的文件又盖回去。rm -rf是最需要敬畏的。我给自己定的规矩是只要带-r和f命令里的路径必须是绝对路径而且必须是从别处复制过来的不能手敲。手敲路径出错率高得离谱一个字符位置不对删的就是另一个东西。更稳妥的做法是先用find把要删的文件列出来确认再交给-delete或-exec rm执行。# 先确认 find /data/tmp -name *.log -mtime 30 -print # 确认无误再删 find /data/tmp -name *.log -mtime 30 -delete注意rm -rf后面跟空格再跟变量是经典事故现场如果变量为空命令会变成rm -rf后面没有对象有些实现会报错退出有些不会。写脚本时务必写成rm -rf -- ${DIR:?}/${VAR:?}的作用是变量为空时直接报错终止。3.3 覆盖之前先看一眼三种看文件的姿势改配置之前先看内容这是基本动作但用哪个命令看有讲究。cat适合短文件直接全打出来less适合长文件能翻页、能搜索进去之后按/输入关键词回车就能定位n找下一个q退出tail -f适合看正在写的日志会持续追加输出。head -n 20和tail -n 20分别看头尾 20 行配置文件的格式通常集中在头部日志的最新状态在尾部。tail -f有一个进阶参数-F比-f更强它在文件被轮转比如日志切割后重新创建时会自动重新打开-f则可能一直盯着一个已经不写的老文件。生产环境看日志我一直用tail -F。还有个场景是看文件有没有被改过。stat命令会列出访问时间、修改时间、状态变更时间md5sum或sha256sum可以算校验值改动前后对比一下就知道内容有没有变。改关键配置前先存一份.bak也是成本极低的做法。4. 权限rwx 和 755 到底在管谁4.1 三个身份乘以三种动作权限模型其实是一张 3×3 的表三种身份属主 u、属组 g、其他人 o各自拥有三种动作读 r、写 w、执行 x。rwxr-xr-x这九位就是这么切出来的前三位属主中三位属组后三位其他人。数字写法是把每组的权限按位换算r4、w2、x1三者的和就是这一组的数字。所以rwx 421 7r-x 401 5r-- 4--- 0。755就是rwxr-xr-x644就是rw-r--r--600就是rw-------。数字权限串典型场景755rwxr-xr-x目录、可执行脚本644rw-r--r--普通配置文件、网页文件600rw-------私钥、含密码的配置700rwx------仅属主可进的家目录777rwxrwxrwx基本不该出现777这个值单独说一句。新手遇到权限问题第一反应就是chmod 777这个操作确实能解决眼前的报错但它意味着任何用户都能改、能删。在共享机器上这等于把门全打开。真正的解法是搞清楚谁需要什么权限把属主或属组调对而不是把权限开到最大。4.2 为什么 sudo 之后还是权限不够这个问题我遇到过很多次现象很迷惑明明用了 sudo还是报 Permission denied。原因通常不在当前用户身上而在目标文件的属性或者上层路径上。常见的三种情况第一种文件被加了不可变属性。chattr i加上的标记连 root 都改不了lsattr能看到那个i标志要先chattr -i才能编辑。这个属性一般是安全加固时设的忘了就会卡很久。第二种上层目录缺执行权限。想访问/data/app/config.yaml路径上的每一层目录都需要有x权限缺一层就进不去。这个坑特别隐蔽因为文件本身的权限看起来完全正常。第三种问题根本不在文件权限上而是目标磁盘只读挂载、SELinux 或 AppArmor 之类的强制访问控制拦截、或者文件系统本身有错误。这几种的报错文本和权限不足很像但ls -l一看权限是正常的就要往其他方向查。4.3 目录的 x 权限是最容易被忽略的文件和目录的rwx含义不一样这点必须单独讲清楚。对文件来说r是读内容、w是改内容、x是能执行。对目录来说r是能列出里面有哪些名字w是能在里面新建或删除条目x是能进入这个目录、能访问里面已知名字的文件。关键的差别在x上。目录没有x权限即使你知道里面文件的完整路径也打不开它。而目录只有x没有r的时候你不能ls列出内容但如果知道确切文件名是可以访问的。这个特性在实际中很有用——共享目录里想让人按路径取文件但不让他翻目录列表就设成--x。另一个容易搞混的是删除的判定。能不能删一个文件取决于它所在目录的w权限而不是文件本身的权限。文件是444只读但所在目录你有w照样能删掉它。这也是为什么/tmp通常设成1777——最后那个1是粘滞位sticky bit保证每个人只能删自己的文件否则谁都往/tmp写又谁都能删别人的东西就乱套了。5. 进程、端口、磁盘卡住了先看这三样5.1 ps 和 top先把吃资源的那个抓出来服务没响应、机器突然变慢、命令敲下去半天没回显这类症状第一件事是看进程。ps aux输出全量进程快照字段比较多重点看三列%CPU、%MEM和COMMAND。想按 CPU 排序ps aux --sort-%cpu | head -10直接给前十个。top是动态的默认三秒刷新一次按P按 CPU 排序、按M按内存排序、按c显示完整命令行、按q退出。看top有几个要点第一行的 load average 三个数字分别是 1 分钟、5 分钟、15 分钟的平均负载要和 CPU 核数对比着看8 核机器 load 到 8 才算跑满第三行的%Cpu(s)里wa是 IO 等待时间这个值高说明瓶颈在磁盘不在 CPU一味加 CPU 是白费力气。ps还有一个用法值得掌握按名字找。ps -ef | grep nginx是老写法更干净的是pgrep -af nginx-a显示完整命令行-f匹配完整命令而不只是进程名。查某个进程打开的文件用lsof -p PID查它监听的端口用ss -lntp | grep PID。5.2 kill 不是只有 -9新手删不掉进程就上kill -9这个习惯不好。kill默认发的是TERM信号 15含义是请你退出进程收到后可以保存状态、关闭连接、清理临时文件然后自己结束。-9是KILL含义是立刻死进程没有机会做任何清理可能留下锁文件、半截写入的数据、没释放的连接。正确的顺序是先kill PID等几秒没反应再kill -9 PID。很多服务在收到TERM后会优雅关闭直接-9反而要花更多时间处理残留问题。还有几件事要一并确认。进程可能已经变成僵尸状态Z这种进程已经结束但父进程没回收kill对它无效要处理的是它的父进程。也可能你杀的是一个 shell 脚本真正的业务进程是它拉起来的子进程杀了父进程子进程还在跑得按进程组处理kill -- -PGID。5.3 磁盘满了的排查顺序磁盘满是最典型也最容易被误判的故障——表现五花八门可能是服务起不来、可能是日志写不进去、可能是数据库报错但根因都是空间耗尽。排查顺序我固定成三步第一步看哪个分区满。df -h一眼就能看出来重点看Use%那一列。如果某个分区到了 95% 以上基本可以锁定。第二步从根往下一层层找大目录。du -h --max-depth1 / 2/dev/null | sort -rh | head -10先看根下哪个一级目录占用最多再进那个目录重复一次两三层就能定位到具体位置。--max-depth控制层数不加会递归到底慢得让人怀疑人生。第三步看是不是删了但没释放。这个坑极隐蔽文件被删除了但还有进程持有它的句柄空间不释放df显示满的du却怎么算都对不上。这时候用lsof L1或lsof | grep deleted找那些已删除但仍被占用的文件重启对应进程就能释放。df -h du -h --max-depth1 /var 2/dev/null | sort -rh | head lsof L1 2/dev/null | head还有一种情况是 inode 满了。df -i看 inode 使用率如果IUse%到 100%磁盘还有空间也写不进新文件。这种通常是某个目录下有海量小文件造成的清理方式和清理空间不同要按文件数量来找。6. 管道和重定向命令行真正的生产力6.1 和 的差别以及那个坑人的 21重定向是把命令的输入输出接到别的地方去。每个进程默认有三个通道标准输入 0、标准输出 1、标准错误 2。把标准输出写到文件并覆盖是追加2把错误输出写到文件把两者都写进去。command out.log 21 # 正确先接标准输出再把错误导向标准输出 command 21 out.log # 错误错误还是打到屏幕第二种写法为什么错值得解释清楚。重定向是从左往右依次生效的21在这一刻的含义是让 2 号指向 1 号当前指向的地方而此时 1 号还指着终端所以错误输出去了终端后面 out.log才把 1 号改到文件。顺序反了结果就完全不同。另一个常用的是/dev/null写进去的东西直接丢弃。command /dev/null 21是完全静默command 2/dev/null是只要正常输出错误丢掉。写脚本时把探测类命令的错误丢掉很常见避免噪音污染输出。6.2 grep、awk、sed 的最小可用集文本处理被称为命令行的生产力核心绕不开这三件。不用一开始就学全掌握最小可用集就能解决八成问题。grep找内容。-r递归目录-n显示行号-i忽略大小写-v反选找不含某关键词的行--include*.conf限定文件类型避免扫二进制。grep -rn --include*.conf timeout /etc | head -20awk按列取。默认按空白分割$1是第一列$NF是最后一列NR是行号。最常见的用法是awk {print $1}取第一列加上-F:可以指定分隔符。awk -F: $3 1000 {print $1, $3} /etc/passwdsed做替换。基本格式sed s/旧/新/gg表示一行内全部替换不加只换第一个。-i直接改文件改之前最好先不加-i跑一遍看看输出确认了再加。sed s/localhost/127.0.0.1/g app.conf # 先预览 sed -i.bak s/localhost/127.0.0.1/g app.conf # 改并留备份sed -i.bak这个写法值得记它会自动生成app.conf.bak改坏了随时能回去。6.3 通配符和 find批量操作的两种思路通配符由 shell 展开*匹配任意长度?匹配单个字符[abc]匹配其中一个{a,b}是花括号展开严格说不是通配符但用起来一样方便。rm *.log就是 shell 先把*.log展开成实际文件名列表再交给rm。find是直接遍历目录树不依赖 shell 展开所以能处理参数过长的问题文件太多时通配符展开会超出命令行长度限制。它的表达式组合非常灵活find /var/log -type f -name *.log -mtime 7 -size 100M-type f只要普通文件-mtime 7修改时间超过 7 天-size 100M大于 100M。三个条件是与的关系。想在结果上执行操作用-exec或-delete前者更灵活find /data -name *.tmp -exec rm {} \;结尾的\;是转义的-exec参数结束符{}是占位符。也可以写成-exec rm {} 效率更高它会把多个文件拼成一条命令而\;是每个文件执行一次。7. 一次完整的排查链路从服务起不来倒推7.1 现象、假设、验证的循环前面几块内容都是零件真正把人拉开差距的是把它们串起来的能力。我拿一个自己遇到过多次的典型场景走一遍某个自建服务突然访问不了。先确认现象边界。是本机访问不了还是外部访问不了curl -v http://127.0.0.1:8080/在本机跑一下如果本地通、外部不通问题在网络层或监听地址如果本地就不通问题在服务本身。再确认进程在不在。pgrep -af app看有没有对应进程ps aux | grep app也行。没有进程说明它挂了去看日志为什么挂有进程但端口不通就往端口方向查。接着看端口有没有监听。ss -lntp | grep 8080。如果显示监听在127.0.0.1:8080那外部自然连不上因为只监听了回环地址如果压根没有 8080 的条目说明服务根本没把端口开起来可能是启动时配置没读到、或者启动失败退出了。然后看日志。服务自己的日志优先tail -F /var/log/app/error.log再往上找系统层journalctl -u app --since 10 min ago。日志里如果有 Address already in use那是端口被占了如果有 Permission denied 打开某个文件那就是权限问题如果是 No space left on device回到上一节看磁盘。最后看资源。df -h看空间free -h看内存top看负载。这三样都正常再考虑网络配置、上游依赖这些更外层的因素。这一圈走下来绝大部分问题都能定位到具体某一层。重要的是这个顺序不能乱从内往外从服务自己到它依赖的系统因为越往外排查成本越高。7.2 把这次的路径固化成习惯单次排查解决的是眼前的事真正有复利的是把路径沉淀成习惯。我自己的做法是维护一份检查清单每次都按同样的顺序走一遍不跳步也不凭直觉直接跳到结论。直觉有时候对但失误的代价比多走两步大得多。清单大致是这样服务状态 → 端口监听 → 日志报错 → 磁盘空间 → 内存与负载 → 路径上的权限 → 上游依赖。七步熟练之后一分钟能走完但不走就可能在一个假象上耗半小时。比如服务假死这个现象看着像应用问题实际可能是磁盘写满导致它卡在某次写操作上如果不查df你会一直在应用代码里打转。另外补一个小技巧留下执行痕迹。排查过程中敲过的每条命令用history都能翻回来但更省事的是把关键几条顺手记在一个临时文件里或者直接复制到工单里。下次遇到同样的现象照着历史复现一遍就行不用重新推理。我见过太多人排完一个问题过两周再遇到又从头开始想。经验不等于记忆能复用才算数。最后分享一个我自己用了很多年的小习惯每次要执行一条心里没底的命令先把它敲在终端里别按回车从头读一遍重点看路径和参数读完再决定。这一两秒的停顿挡掉过的麻烦比我学过的所有高级技巧加起来都多。命令行的门槛从来不在命令数量而在于每一步都清楚自己站在哪、动的是什么。