-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatom.xml
More file actions
396 lines (396 loc) · 327 KB
/
Copy pathatom.xml
File metadata and controls
396 lines (396 loc) · 327 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<author>
<name>Code Water</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<id>https://blog.codewater.top/</id>
<link href="https://blog.codewater.top/" rel="alternate"/>
<link href="https://blog.codewater.top/atom.xml" rel="self"/>
<rights>All rights reserved 2026, Code Water</rights>
<subtitle>阿水</subtitle>
<title>Code Water</title>
<updated>2026-07-20T04:08:24.000Z</updated>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="Docker" scheme="https://blog.codewater.top/tags/Docker/"/>
<category term="容器" scheme="https://blog.codewater.top/tags/%E5%AE%B9%E5%99%A8/"/>
<category term="部署" scheme="https://blog.codewater.top/tags/%E9%83%A8%E7%BD%B2/"/>
<category term="效率工具" scheme="https://blog.codewater.top/tags/%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7/"/>
<content>
<![CDATA[<p>“我这儿能跑啊”——部署最老的敌人就是环境差异:你机器上有的依赖、库版本、系统配置,服务器上不一定有。Docker 的解法是把程序连同它的整个运行环境(系统库、依赖、配置)打包成一个可移植的”盒子”,在哪台机器上跑,环境都一模一样。</p><span id="more"></span><h2 id="Docker-是什么:三个核心概念"><a href="#Docker-是什么:三个核心概念" class="headerlink" title="Docker 是什么:三个核心概念"></a><a href="https://docs.docker.com/">Docker</a> 是什么:三个核心概念</h2><p>Docker 是容器引擎——用比虚拟机轻得多的方式隔离和运行程序。上手前必须分清三个概念,绝大多数新手困惑都源于把前两个混为一谈:</p><ul><li><strong>镜像(image)</strong>:只读的模板,包含程序和它的完整运行环境——类比面向对象里的”类”</li><li><strong>容器(container)</strong>:镜像跑起来的实例——类比”对象”,同一个镜像可以同时跑出多个互不干扰的容器</li><li><strong>仓库(registry)</strong>:集中存放镜像的服务,默认是 Docker Hub,<code>docker pull</code> 从这里拉镜像</li></ul><pre><code class="highlight mermaid">flowchart LR D[Dockerfile] -->|docker build| I[镜像 image] I -->|docker run| C1[容器 1] I -->|docker run| C2[容器 2] R[(仓库 registry)] -->|docker pull| I I -->|docker push| R</code></pre><h2 id="安装:一条命令"><a href="#安装:一条命令" class="headerlink" title="安装:一条命令"></a>安装:一条命令</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://get.docker.com | sh</span><br><span class="line"><span class="comment"># Linux 上的官方便捷安装脚本——-sSL 这套管道安装的参数含义在 curl 那篇讲过</span></span><br><span class="line"><span class="comment"># -f:HTTP 出错时直接失败,不把错误页当脚本喂给 sh</span></span><br><span class="line"></span><br><span class="line">docker run hello-world</span><br><span class="line"><span class="comment"># 装完跑这个验证:能输出欢迎信息说明引擎正常</span></span><br></pre></td></tr></table></figure><p>macOS/Windows 用 <a href="https://www.docker.com/products/docker-desktop/">Docker Desktop</a> 图形化安装(macOS 上轻量替代 OrbStack 也很流行),装完命令行用法完全一致。</p><h2 id="docker-run:参数拆解"><a href="#docker-run:参数拆解" class="headerlink" title="docker run:参数拆解"></a>docker run:参数拆解</h2><p>跑一个 nginx,把最常用的参数一次讲清:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">docker run -d -p 8080:80 --name web nginx</span><br><span class="line"><span class="comment"># nginx:镜像名,本地没有会自动去仓库拉取</span></span><br><span class="line"><span class="comment"># -d:detached,后台运行,不占着当前终端</span></span><br><span class="line"><span class="comment"># -p 8080:80:端口映射,方向是 宿主机:容器——</span></span><br><span class="line"><span class="comment"># 把宿主机的 8080 映射到容器里的 80,浏览器访问 localhost:8080 就到了容器里的 nginx</span></span><br><span class="line"><span class="comment"># 方向记反是最常见的新手错误:冒号左边永远是你机器上的端口</span></span><br><span class="line"><span class="comment"># --name web:给容器起名,后续操作用名字代替随机生成的 ID</span></span><br></pre></td></tr></table></figure><h2 id="容器生命周期:从跑起来到删干净"><a href="#容器生命周期:从跑起来到删干净" class="headerlink" title="容器生命周期:从跑起来到删干净"></a>容器生命周期:从跑起来到删干净</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">docker ps</span><br><span class="line"><span class="comment"># 列出正在运行的容器</span></span><br><span class="line"></span><br><span class="line">docker ps -a</span><br><span class="line"><span class="comment"># -a:包含已退出的容器——容器停了不等于没了,还占着名字和磁盘</span></span><br><span class="line"></span><br><span class="line">docker logs -f web</span><br><span class="line"><span class="comment"># 看容器的输出日志,-f 实时跟踪(和 tail -f 一个手感)</span></span><br><span class="line"></span><br><span class="line">docker <span class="built_in">exec</span> -it web sh</span><br><span class="line"><span class="comment"># 进入运行中的容器内部开个 shell,排查问题用</span></span><br><span class="line"><span class="comment"># -i 保持输入、-t 分配终端,基本固定连用;exit 退出不影响容器运行</span></span><br><span class="line"></span><br><span class="line">docker stop web</span><br><span class="line"><span class="comment"># 停止容器(先发 SIGTERM 优雅退出,超时再强杀——和 systemctl stop 同款逻辑)</span></span><br><span class="line"></span><br><span class="line">docker <span class="built_in">rm</span> web</span><br><span class="line"><span class="comment"># 删除已停止的容器;运行中的要先 stop 或用 rm -f</span></span><br><span class="line"></span><br><span class="line">docker run --<span class="built_in">rm</span> -it golang:1.26 bash</span><br><span class="line"><span class="comment"># --rm:容器退出后自动删除自己——临时起个环境试点东西,用完不留垃圾</span></span><br></pre></td></tr></table></figure><p>一个必须建立的认知:<strong>容器不是轻量虚拟机,它的生命周期跟着主进程走</strong>——<code>docker run</code> 启动时指定(或镜像默认)的那个进程退出了,容器就停了。跑个 <code>docker run ubuntu</code> 发现容器”秒退”,不是坏了,是它的默认命令执行完就结束了。</p><h2 id="数据坑:容器删了,数据跟着没了"><a href="#数据坑:容器删了,数据跟着没了" class="headerlink" title="数据坑:容器删了,数据跟着没了"></a>数据坑:容器删了,数据跟着没了</h2><p>容器内部写的文件属于容器自己的可写层,<code>docker rm</code> 之后整层蒸发。数据库这类有状态的服务,必须把数据目录挂载到宿主机:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">docker run -d --name pg \</span><br><span class="line"> -e POSTGRES_PASSWORD=devpass \</span><br><span class="line"> -v /data/pgdata:/var/lib/postgresql/data \</span><br><span class="line"> postgres:17</span><br><span class="line"><span class="comment"># -e:设置容器内的环境变量,镜像文档会列出支持哪些</span></span><br><span class="line"><span class="comment"># -v 宿主路径:容器路径:把宿主机目录挂进容器——</span></span><br><span class="line"><span class="comment"># 数据实际写在宿主机 /data/pgdata,容器删了重建,数据还在</span></span><br></pre></td></tr></table></figure><p>判断标准很简单:这个容器产生的数据丢了心不心疼?心疼就必须 <code>-v</code> 挂出来。</p><h2 id="镜像管理与-tag"><a href="#镜像管理与-tag" class="headerlink" title="镜像管理与 tag"></a>镜像管理与 tag</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">docker images</span><br><span class="line"><span class="comment"># 列出本地已有的镜像和它们的体积</span></span><br><span class="line"></span><br><span class="line">docker pull nginx:1.27</span><br><span class="line"><span class="comment"># 拉取指定版本,冒号后面是 tag(版本标签)</span></span><br><span class="line"></span><br><span class="line">docker rmi nginx:1.27</span><br><span class="line"><span class="comment"># 删除本地镜像,释放磁盘</span></span><br></pre></td></tr></table></figure><p>tag 有个约定俗成的坑:不写 tag 默认拉 <code>latest</code>,但 latest 只是个普通标签,指向的版本会随时间漂移——今天构建和明天构建拉到的可能不是同一个东西。<strong>生产环境永远写死具体版本号</strong>,<code>postgres:17</code> 而不是 <code>postgres:latest</code>。</p><h2 id="Dockerfile:把自己的-Go-服务打成镜像"><a href="#Dockerfile:把自己的-Go-服务打成镜像" class="headerlink" title="Dockerfile:把自己的 Go 服务打成镜像"></a>Dockerfile:把自己的 Go 服务打成镜像</h2><p>Dockerfile 是构建镜像的”配方”。Go 程序编译成单个静态二进制,特别适合多阶段构建——编译在带完整工具链的大镜像里做,运行只带二进制本身:</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 阶段一:编译。golang 官方镜像自带完整编译工具链,体积几百 MB(Debian 底座的接近 1 GB),但只在构建时用</span></span><br><span class="line"><span class="keyword">FROM</span> golang:<span class="number">1.26</span>-alpine AS builder</span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> go.mod go.sum ./</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> go mod download</span></span><br><span class="line"><span class="comment"># 先只拷贝依赖清单并下载依赖——这两层能吃缓存,</span></span><br><span class="line"><span class="comment"># 代码改动不会导致依赖重新下载,构建快很多</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> . .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> CGO_ENABLED=0 go build -o server .</span></span><br><span class="line"><span class="comment"># CGO_ENABLED=0:纯静态编译,产物不依赖任何系统库</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 阶段二:运行。只把二进制拷进一个几 MB 的空底座</span></span><br><span class="line"><span class="keyword">FROM</span> alpine:latest</span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> --from=builder /app/server .</span></span><br><span class="line"><span class="comment"># --from=builder:从上一阶段拷贝产物,工具链、源码统统不带</span></span><br><span class="line"><span class="keyword">EXPOSE</span> <span class="number">8080</span></span><br><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [<span class="string">"./server"</span>]</span></span><br><span class="line"><span class="comment"># CMD:容器启动时执行的命令——就是前面说的"主进程"</span></span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">docker build -t myapp:1.0 .</span><br><span class="line"><span class="comment"># -t:给构建出的镜像打名字和 tag;最后的 . 是构建上下文(Dockerfile 所在目录)</span></span><br><span class="line"></span><br><span class="line">docker run -d -p 8080:8080 myapp:1.0</span><br></pre></td></tr></table></figure><p>多阶段构建的效果立竿见影:单阶段直接用 golang 镜像跑,成品接近 1 GB;两阶段下来通常只有十几 MB。</p><p>为什么差距这么大?关键在于<strong>镜像是一层层叠出来的,最终镜像 = 基础镜像的所有层 + 你新加的层,叠上去的东西删不掉</strong>。单阶段构建时基础镜像是 golang——里面装着 Go 编译器、标准库、git、gcc 这一整套构建工具(Debian 底座解压后就有 800 MB 上下),再叠上 <code>go mod download</code> 拉下来的依赖缓存和你的源码,成品轻松逼近 1 GB。可服务跑起来真正需要的只有那个十几 MB 的静态二进制,剩下全是”编译完就没用了”的东西。多阶段构建的本质就是把这些甩掉:<code>FROM alpine</code> 另起一个只有几 MB 的干净底座,只把二进制 <code>COPY</code> 进去——builder 阶段的所有层都不会进入最终镜像。</p><h2 id="docker-compose:多容器一把梭"><a href="#docker-compose:多容器一把梭" class="headerlink" title="docker compose:多容器一把梭"></a>docker compose:多容器一把梭</h2><p>服务一多(web + 数据库 + 缓存),逐个 <code>docker run</code> 又长又难维护。compose 把整套服务声明在一个 <code>compose.yaml</code> 里:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line"> <span class="attr">web:</span></span><br><span class="line"> <span class="attr">build:</span> <span class="string">.</span></span><br><span class="line"> <span class="attr">ports:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">"8080:8080"</span></span><br><span class="line"> <span class="attr">environment:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">REDIS_ADDR=redis:6379</span></span><br><span class="line"> <span class="attr">depends_on:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">redis</span></span><br><span class="line"> <span class="attr">redis:</span></span><br><span class="line"> <span class="attr">image:</span> <span class="string">redis:7</span></span><br><span class="line"> <span class="attr">volumes:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">./redis-data:/data</span></span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">docker compose up -d</span><br><span class="line"><span class="comment"># 一条命令把整套服务按依赖顺序拉起来</span></span><br><span class="line"><span class="comment"># 注意是 docker compose(空格)——v2 的写法,作为插件内置在现代 Docker 里</span></span><br><span class="line"><span class="comment"># 老文章里的 docker-compose(连字符)是 v1 独立程序,2023 年就停止更新了</span></span><br><span class="line"></span><br><span class="line">docker compose logs -f</span><br><span class="line"><span class="comment"># 聚合看所有服务的日志</span></span><br><span class="line"></span><br><span class="line">docker compose down</span><br><span class="line"><span class="comment"># 整套停掉并删除容器(挂载出来的数据不受影响)</span></span><br></pre></td></tr></table></figure><p>compose 文件里服务名(如 <code>redis</code>)自动成为容器间互相访问的主机名——<code>web</code> 里连 <code>redis:6379</code> 就通,不用关心容器 IP。</p><h2 id="排查与查看:network、inspect-与-docker-info"><a href="#排查与查看:network、inspect-与-docker-info" class="headerlink" title="排查与查看:network、inspect 与 docker info"></a>排查与查看:network、inspect 与 docker info</h2><p>容器出问题时光看 logs 不够,还得能看它的网络接在哪、完整配置是什么、引擎本身的状态如何。这几个都是只读的查看类命令,放心随便敲:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">docker network <span class="built_in">ls</span></span><br><span class="line"><span class="comment"># 列出 Docker 的网络。默认有三个:bridge(容器不指定网络就接它)、host、none</span></span><br><span class="line"></span><br><span class="line">docker network inspect bridge</span><br><span class="line"><span class="comment"># 看某个网络的详情:网段、网关、接在这个网络上的容器和它们各自的 IP</span></span><br><span class="line"></span><br><span class="line">docker inspect web</span><br><span class="line"><span class="comment"># 输出容器的完整配置 JSON:环境变量、挂载、端口映射、IP、重启策略全在里面</span></span><br><span class="line"><span class="comment"># 镜像同样能 inspect:docker inspect nginx:1.27</span></span><br><span class="line"></span><br><span class="line">docker inspect -f <span class="string">'{{.NetworkSettings.IPAddress}}'</span> web</span><br><span class="line"><span class="comment"># -f:format,用 Go 模板语法只取想要的字段——这个例子直接输出容器的 IP</span></span><br><span class="line"></span><br><span class="line">docker port web</span><br><span class="line"><span class="comment"># 只看这个容器的端口映射,比在 inspect 的大 JSON 里翻快得多</span></span><br><span class="line"></span><br><span class="line">docker info</span><br><span class="line"><span class="comment"># Docker 引擎自身的配置和状态:存储驱动、数据根目录、容器/镜像数量、cgroup 版本</span></span><br><span class="line"></span><br><span class="line">docker stats</span><br><span class="line"><span class="comment"># 实时看各容器的 CPU、内存、网络占用——容器版的 top</span></span><br><span class="line"></span><br><span class="line">docker system <span class="built_in">df</span></span><br><span class="line"><span class="comment"># 镜像、容器、数据卷各占了多少磁盘,清理前先看这个</span></span><br></pre></td></tr></table></figure><h2 id="常用命令速查表"><a href="#常用命令速查表" class="headerlink" title="常用命令速查表"></a>常用命令速查表</h2><table><thead><tr><th>命令</th><th>作用</th></tr></thead><tbody><tr><td><code>docker run -d 镜像</code></td><td>后台启动容器</td></tr><tr><td><code>docker run -p 宿主:容器</code></td><td>端口映射,冒号左边是宿主机</td></tr><tr><td><code>docker run -v 宿主:容器</code></td><td>目录挂载,有状态数据必挂</td></tr><tr><td><code>docker run -e KEY=VAL</code></td><td>设置容器内环境变量</td></tr><tr><td><code>docker run --name 名字</code></td><td>给容器命名</td></tr><tr><td><code>docker run --rm</code></td><td>退出后自动删除,临时容器用</td></tr><tr><td><code>docker run -it 镜像 sh</code></td><td>交互式进入容器 shell</td></tr><tr><td><code>docker ps</code> / <code>ps -a</code></td><td>运行中的容器 / 含已退出的</td></tr><tr><td><code>docker logs -f 容器</code></td><td>实时跟踪容器日志</td></tr><tr><td><code>docker exec -it 容器 sh</code></td><td>进入运行中的容器</td></tr><tr><td><code>docker stop</code> / <code>rm</code></td><td>停止 / 删除容器</td></tr><tr><td><code>docker images</code></td><td>本地镜像列表</td></tr><tr><td><code>docker pull 镜像:tag</code></td><td>拉取镜像,生产写死具体 tag</td></tr><tr><td><code>docker rmi 镜像</code></td><td>删除本地镜像</td></tr><tr><td><code>docker build -t 名:tag .</code></td><td>按 Dockerfile 构建镜像</td></tr><tr><td><code>docker network ls</code></td><td>列出网络</td></tr><tr><td><code>docker network inspect 网络</code></td><td>网络详情:网段、网关、容器 IP</td></tr><tr><td><code>docker inspect 容器/镜像</code></td><td>完整配置 JSON</td></tr><tr><td><code>docker inspect -f 模板</code></td><td>只取 JSON 里指定的字段</td></tr><tr><td><code>docker port 容器</code></td><td>只看端口映射</td></tr><tr><td><code>docker info</code></td><td>引擎配置与状态</td></tr><tr><td><code>docker stats</code></td><td>实时资源占用,容器版 top</td></tr><tr><td><code>docker system df</code></td><td>镜像/容器/数据卷磁盘占用</td></tr><tr><td><code>docker compose up -d</code></td><td>按 compose.yaml 拉起整套服务</td></tr><tr><td><code>docker compose down</code></td><td>整套停掉并删容器</td></tr><tr><td><code>docker compose logs -f</code></td><td>聚合跟踪所有服务日志</td></tr></tbody></table><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p>Docker 上手的关键是概念先行:镜像是模板、容器是实例、容器生死跟着主进程走、心疼的数据必须挂载出来。这四句话立住之后,命令都只是查表的事。Dockerfile 多阶段构建和 compose 是从”会用”到”用好”的两步——前者让镜像从 GB 瘦到 MB,后者让多服务环境一条命令起停。再往深走就是网络模型和编排(Kubernetes)的领域了,日常开发部署,本文这套已经够用。</p>]]>
</content>
<id>https://blog.codewater.top/Docker%E6%95%99%E7%A8%8B-%E9%95%9C%E5%83%8F%E5%AE%B9%E5%99%A8%E6%A6%82%E5%BF%B5%E5%88%B0Dockerfile%E4%B8%8Ecompose%E5%AE%9E%E6%88%98/</id>
<link href="https://blog.codewater.top/Docker%E6%95%99%E7%A8%8B-%E9%95%9C%E5%83%8F%E5%AE%B9%E5%99%A8%E6%A6%82%E5%BF%B5%E5%88%B0Dockerfile%E4%B8%8Ecompose%E5%AE%9E%E6%88%98/"/>
<published>2026-07-19T16:12:02.000Z</published>
<summary>镜像、容器、仓库三个概念一张图讲清,docker run 参数拆解、数据挂载、Go 多阶段构建与 compose 入门。</summary>
<title>Docker 教程:从镜像容器概念到 Dockerfile 与 compose 实战</title>
<updated>2026-07-20T04:08:24.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="效率工具" scheme="https://blog.codewater.top/tags/%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="终端" scheme="https://blog.codewater.top/tags/%E7%BB%88%E7%AB%AF/"/>
<category term="tmux" scheme="https://blog.codewater.top/tags/tmux/"/>
<content>
<![CDATA[<p>SSH 上服务器跑了两个小时的数据迁移,网一抖连接断了——任务跟着一起被杀死,从头再来。问题的根源是:普通终端里,你跑的所有程序都挂在这条 SSH 连接下面,连接断了它们就成了孤儿被系统清理。tmux 解决的正是这件事:把”干活的会话”和”网络连接”解耦,连接断了会话照常活着。</p><span id="more"></span><p>这是终端工具系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>。</p><h2 id="tmux-是什么:终端复用器与三层概念"><a href="#tmux-是什么:终端复用器与三层概念" class="headerlink" title="tmux 是什么:终端复用器与三层概念"></a><a href="https://github.com/tmux/tmux/wiki">tmux</a> 是什么:终端复用器与三层概念</h2><p>tmux(terminal multiplexer,终端复用器)在终端和你跑的程序之间加了一层”会话服务器”:程序挂在 tmux 的会话下而不是 SSH 连接下,所以连接断开、终端关闭都不影响它们继续跑。装起来一句话:<code>apt install tmux</code>(Linux)或 <code>brew install tmux</code>(macOS)。</p><p>它的界面组织分三层,从大到小:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">session(会话) 一个独立的工作环境,可以整体分离和恢复——tmux 的灵魂</span><br><span class="line"> └─ window(窗口) 会话里的"标签页",一个会话可以开多个,像浏览器 tab</span><br><span class="line"> └─ pane(窗格) 窗口再切分出的小块屏幕,一屏同时看多个命令的输出</span><br></pre></td></tr></table></figure><p>日常最常用的是 session 这一层(保命)和 pane 这一层(分屏),window 介于两者之间按需使用。</p><h2 id="核心场景:断线不丢任务"><a href="#核心场景:断线不丢任务" class="headerlink" title="核心场景:断线不丢任务"></a>核心场景:断线不丢任务</h2><p>先把最值钱的工作流走一遍:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">tmux new -s migrate</span><br><span class="line"><span class="comment"># 新建一个名叫 migrate 的会话,进入后看到的还是普通 shell</span></span><br><span class="line"><span class="comment"># -s:session 名字,起个有意义的名字,恢复时好认</span></span><br><span class="line"></span><br><span class="line">./run-migration.sh</span><br><span class="line"><span class="comment"># 在会话里正常跑你的长任务</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 按 Ctrl-b 再按 d —— 分离(detach)会话</span></span><br><span class="line"><span class="comment"># 回到了原来的终端,但会话和里面的任务还在后台跑着</span></span><br><span class="line"><span class="comment"># 这时候断开 SSH、合上笔记本、下班,都不影响它</span></span><br><span class="line"></span><br><span class="line">tmux <span class="built_in">ls</span></span><br><span class="line"><span class="comment"># 下次连上服务器,先列出所有活着的会话</span></span><br><span class="line"><span class="comment"># migrate: 1 windows (created ...) —— 还活着</span></span><br><span class="line"></span><br><span class="line">tmux attach -t migrate</span><br><span class="line"><span class="comment"># -t:target,重新接上这个会话——屏幕内容、运行状态和离开时一模一样</span></span><br></pre></td></tr></table></figure><p>两个补充命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">tmux new -A -s work</span><br><span class="line"><span class="comment"># -A:会话已存在就直接附加,不存在才新建——写进登录脚本很合适,</span></span><br><span class="line"><span class="comment"># 每次连服务器自动进入同一个工作环境</span></span><br><span class="line"></span><br><span class="line">tmux kill-session -t migrate</span><br><span class="line"><span class="comment"># 任务彻底完了,删掉这个会话</span></span><br></pre></td></tr></table></figure><h2 id="prefix-前缀键:所有快捷键的入口"><a href="#prefix-前缀键:所有快捷键的入口" class="headerlink" title="prefix 前缀键:所有快捷键的入口"></a>prefix 前缀键:所有快捷键的入口</h2><p>tmux 的快捷键都是”两段式”:先按前缀键 <code>Ctrl-b</code>(按完松开),再按功能键。前面用到的分离就是 <code>Ctrl-b</code> 然后 <code>d</code>。忘了某个键绑定时,<code>Ctrl-b ?</code> 列出全部快捷键,<code>q</code> 退出查看。</p><p>下文的快捷键都省略前缀,写作”<code>Ctrl-b</code> + 键”。</p><h2 id="window:会话里的标签页"><a href="#window:会话里的标签页" class="headerlink" title="window:会话里的标签页"></a>window:会话里的标签页</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Ctrl-b c 新建窗口(create)</span><br><span class="line">Ctrl-b , 重命名当前窗口——底部状态栏会显示名字,多窗口时不迷路</span><br><span class="line">Ctrl-b 0-9 按编号直接跳转</span><br><span class="line">Ctrl-b n / p 下一个 / 上一个窗口(next/previous)</span><br><span class="line">Ctrl-b w 所有窗口的列表预览,回车跳转</span><br><span class="line">Ctrl-b & 关闭当前窗口(会确认)</span><br></pre></td></tr></table></figure><p>典型用法:窗口 0 跑服务、窗口 1 看日志、窗口 2 敲命令,<code>Ctrl-b 0/1/2</code> 来回跳。</p><h2 id="pane:一屏切多块"><a href="#pane:一屏切多块" class="headerlink" title="pane:一屏切多块"></a>pane:一屏切多块</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Ctrl-b % 左右分屏(竖着切一刀)</span><br><span class="line">Ctrl-b " 上下分屏(横着切一刀)</span><br><span class="line">Ctrl-b 方向键 在窗格之间移动光标焦点</span><br><span class="line">Ctrl-b z 放大当前窗格到全屏,再按一次恢复分屏布局(zoom)</span><br><span class="line">Ctrl-b x 关闭当前窗格(会确认)</span><br><span class="line">Ctrl-b q 闪出每个窗格的编号,接着按数字可跳过去</span><br></pre></td></tr></table></figure><p><code>%</code> 和 <code>"</code> 这两个键位不太直观,可以这么记:<code>%</code> 这个符号中间是一条斜线把两个圈分开——竖着切;<code>"</code> 是两个点并排躺着——横着切。</p><p><code>Ctrl-b z</code> 值得单独点名:分屏跑着四块日志,想凑近看其中一块,<code>z</code> 放大到全屏,看完再 <code>z</code> 弹回原布局——比关掉重开窗格优雅得多。</p><h2 id="滚屏问题:为什么鼠标滚轮不管用"><a href="#滚屏问题:为什么鼠标滚轮不管用" class="headerlink" title="滚屏问题:为什么鼠标滚轮不管用"></a>滚屏问题:为什么鼠标滚轮不管用</h2><p>新手进 tmux 的第一个困惑:想往上翻看输出,鼠标滚轮却不动历史内容。因为 tmux 默认不把滚轮事件交给历史缓冲区,翻历史要进 copy mode:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Ctrl-b [ 进入 copy mode,此时可以用方向键 / PgUp / PgDn 翻历史</span><br><span class="line">q 退出 copy mode,回到实时画面</span><br></pre></td></tr></table></figure><p>不想记这个的话,一行配置让鼠标直接可用(推荐):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 写进 ~/.tmux.conf</span></span><br><span class="line"><span class="built_in">set</span> -g mouse on</span><br><span class="line"><span class="comment"># 滚轮翻历史、点击切换窗格、拖动调窗格大小全部生效</span></span><br></pre></td></tr></table></figure><h2 id="最小可用配置:-tmux-conf"><a href="#最小可用配置:-tmux-conf" class="headerlink" title="最小可用配置:~/.tmux.conf"></a>最小可用配置:~/.tmux.conf</h2><p>tmux 开箱即用,配置文件不是必需的;但这两行值得一开始就加上:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">set</span> -g mouse on</span><br><span class="line"><span class="comment"># 鼠标支持,上一节说过</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">set</span> -g base-index 1</span><br><span class="line"><span class="comment"># 窗口编号从 1 开始而不是 0——Ctrl-b 1 比 Ctrl-b 0 顺手,键盘上 1 也更近</span></span><br></pre></td></tr></table></figure><p>改完配置后,在 tmux 里执行 <code>tmux source-file ~/.tmux.conf</code> 立即生效,不用重启会话。</p><h2 id="命令行操作速查表"><a href="#命令行操作速查表" class="headerlink" title="命令行操作速查表"></a>命令行操作速查表</h2><table><thead><tr><th>命令</th><th>作用</th></tr></thead><tbody><tr><td><code>tmux new -s 名字</code></td><td>新建命名会话</td></tr><tr><td><code>tmux new -A -s 名字</code></td><td>有则附加、无则新建,适合写进登录脚本</td></tr><tr><td><code>tmux ls</code></td><td>列出所有会话</td></tr><tr><td><code>tmux attach -t 名字</code></td><td>附加到指定会话</td></tr><tr><td><code>tmux kill-session -t 名字</code></td><td>删除指定会话</td></tr><tr><td><code>tmux source-file ~/.tmux.conf</code></td><td>重新加载配置</td></tr></tbody></table><h2 id="快捷键速查表(均先按-Ctrl-b-前缀)"><a href="#快捷键速查表(均先按-Ctrl-b-前缀)" class="headerlink" title="快捷键速查表(均先按 Ctrl-b 前缀)"></a>快捷键速查表(均先按 Ctrl-b 前缀)</h2><table><thead><tr><th>键</th><th>作用</th></tr></thead><tbody><tr><td><code>d</code></td><td>分离会话,任务继续后台跑</td></tr><tr><td><code>?</code></td><td>列出全部快捷键</td></tr><tr><td><code>c</code> / <code>,</code> / <code>&</code></td><td>新建 / 重命名 / 关闭窗口</td></tr><tr><td><code>0-9</code> / <code>n</code> / <code>p</code> / <code>w</code></td><td>跳转 / 下一个 / 上一个 / 列表预览窗口</td></tr><tr><td><code>%</code> / <code>"</code></td><td>左右分屏 / 上下分屏</td></tr><tr><td>方向键</td><td>切换窗格焦点</td></tr><tr><td><code>z</code></td><td>当前窗格全屏/恢复切换</td></tr><tr><td><code>x</code> / <code>q</code></td><td>关闭窗格 / 显示窗格编号</td></tr><tr><td><code>[</code></td><td>进入 copy mode 翻历史,<code>q</code> 退出</td></tr></tbody></table><h2 id="个人推荐配置"><a href="#个人推荐配置" class="headerlink" title="个人推荐配置"></a>个人推荐配置</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># ~/.tmux.conf</span></span><br><span class="line">set-option -g status-keys vi</span><br><span class="line">setw -g mode-keys vi</span><br><span class="line"></span><br><span class="line">setw -g monitor-activity on</span><br><span class="line"></span><br><span class="line"><span class="comment"># setw -g c0-change-trigger 10</span></span><br><span class="line"><span class="comment"># setw -g c0-change-interval 100</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># setw -g c0-change-interval 50</span></span><br><span class="line"><span class="comment"># setw -g c0-change-trigger 75</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">set-window-option -g automatic-rename on</span><br><span class="line">set-option -g set-titles on</span><br><span class="line"><span class="built_in">set</span> -g history-limit 100000</span><br><span class="line"></span><br><span class="line"><span class="comment">#set-window-option -g utf8 on</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># set command prefix</span></span><br><span class="line"><span class="comment">#set-option -g prefix C-a</span></span><br><span class="line"><span class="comment">#unbind-key C-b</span></span><br><span class="line"><span class="comment">#bind-key C-a send-prefix</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">bind</span> h select-pane -L</span><br><span class="line"><span class="built_in">bind</span> j select-pane -D</span><br><span class="line"><span class="built_in">bind</span> k select-pane -U</span><br><span class="line"><span class="built_in">bind</span> l select-pane -R</span><br><span class="line"></span><br><span class="line"><span class="built_in">bind</span> -n M-Left select-pane -L</span><br><span class="line"><span class="built_in">bind</span> -n M-Right select-pane -R</span><br><span class="line"><span class="built_in">bind</span> -n M-Up select-pane -U</span><br><span class="line"><span class="built_in">bind</span> -n M-Down select-pane -D</span><br><span class="line"></span><br><span class="line"><span class="built_in">bind</span> < resize-pane -L 7</span><br><span class="line"><span class="built_in">bind</span> > resize-pane -R 7</span><br><span class="line"><span class="built_in">bind</span> - resize-pane -D 7</span><br><span class="line"><span class="built_in">bind</span> + resize-pane -U 7</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">bind-key -n M-l next-window</span><br><span class="line">bind-key -n M-h previous-window</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="built_in">set</span> -g status-interval 1</span><br><span class="line"><span class="comment"># status bar</span></span><br><span class="line"><span class="built_in">set</span> -g status-bg black</span><br><span class="line"><span class="built_in">set</span> -g status-fg blue</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">#set -g status-utf8 on</span></span><br><span class="line"><span class="built_in">set</span> -g status-justify centre</span><br><span class="line"><span class="built_in">set</span> -g status-bg default</span><br><span class="line"><span class="built_in">set</span> -g status-left <span class="string">" #[fg=green]#S@#H #[default]"</span></span><br><span class="line"><span class="built_in">set</span> -g status-left-length 20</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># mouse support</span></span><br><span class="line"><span class="comment"># for tmux 2.1</span></span><br><span class="line"><span class="comment"># set -g mouse-utf8 on</span></span><br><span class="line"><span class="built_in">set</span> -g mouse on</span><br><span class="line"><span class="comment">#</span></span><br><span class="line"><span class="comment"># for previous version</span></span><br><span class="line"><span class="comment">#set -g mode-mouse on</span></span><br><span class="line"><span class="comment">#set -g mouse-resize-pane on</span></span><br><span class="line"><span class="comment">#set -g mouse-select-pane on</span></span><br><span class="line"><span class="comment">#set -g mouse-select-window on</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">#set -g status-right-length 25</span></span><br><span class="line"><span class="built_in">set</span> -g status-right <span class="string">"#[fg=green]%H:%M:%S #[fg=magenta]%a %m-%d #[default]"</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># fix for tmux 1.9</span></span><br><span class="line"><span class="built_in">bind</span> <span class="string">'"'</span> split-window -vc <span class="string">"#{pane_current_path}"</span></span><br><span class="line"><span class="built_in">bind</span> <span class="string">'%'</span> split-window -hc <span class="string">"#{pane_current_path}"</span></span><br><span class="line"><span class="built_in">bind</span> <span class="string">'c'</span> new-window -c <span class="string">"#{pane_current_path}"</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># run-shell "powerline-daemon -q"</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># vim: ft=conf</span></span><br></pre></td></tr></table></figure><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p>tmux 的学习路径建议按价值排序:先把”new → 干活 → d 分离 → attach 恢复”这条保命链路焊进肌肉记忆(<a href="/SSH%E5%85%8D%E5%AF%86%E7%99%BB%E5%BD%95%E4%B8%8Escp%E5%AE%9E%E6%88%98-%E5%AF%86%E9%92%A5%E9%85%8D%E7%BD%AE%E4%B8%8E%E4%BC%A0%E6%96%87%E4%BB%B6">SSH</a> 断线从此只是小插曲),再加上 <code>set -g mouse on</code> 解决滚屏,最后按需拾起分屏快捷键。终端环境本身的配置(模拟器、shell 框架)另见 <a href="/Mac%E7%BB%88%E7%AB%AF%E5%9F%BA%E7%A1%80-kitty%E5%92%8CZim">kitty + Zim 那篇</a>,和 tmux 是互补的两层。</p>]]>
</content>
<id>https://blog.codewater.top/tmux%E6%95%99%E7%A8%8B-SSH%E6%96%AD%E7%BA%BF%E4%B8%8D%E4%B8%A2%E4%BB%BB%E5%8A%A1%E4%B8%8E%E5%88%86%E5%B1%8F%E4%BC%9A%E8%AF%9D%E7%AE%A1%E7%90%86/</id>
<link href="https://blog.codewater.top/tmux%E6%95%99%E7%A8%8B-SSH%E6%96%AD%E7%BA%BF%E4%B8%8D%E4%B8%A2%E4%BB%BB%E5%8A%A1%E4%B8%8E%E5%88%86%E5%B1%8F%E4%BC%9A%E8%AF%9D%E7%AE%A1%E7%90%86/"/>
<published>2026-07-19T15:51:32.000Z</published>
<summary>tmux 让 SSH 断线不丢任务:session 分离与恢复、window/pane 分屏、滚屏与鼠标配置。</summary>
<title>tmux 教程:SSH 断线不丢任务、分屏与会话管理</title>
<updated>2026-07-19T15:51:32.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="命令行" scheme="https://blog.codewater.top/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
<category term="文本处理" scheme="https://blog.codewater.top/tags/%E6%96%87%E6%9C%AC%E5%A4%84%E7%90%86/"/>
<content>
<![CDATA[<p><code>tail -f</code> 盯着日志排查问题,半夜日志明明还在滚,屏幕却突然静止不动了——日志没停,是 <code>tail -f</code> 被日志轮转甩掉了。这是这对”最简单的命令”里藏得最深的坑,放在最后讲;先把 head 和 tail 本身理顺。</p><span id="more"></span><p>这是 Linux 命令系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>。<code>head</code> 输出文件(或管道输入)的开头部分,<code>tail</code> 输出结尾部分,默认都是 10 行——两个天生一对的小工具,和 <a href="/cut-sort-uniq-wc-jq%E6%96%87%E6%9C%AC%E5%B0%8F%E5%B7%A5%E5%85%B7%E7%BB%84%E5%90%88%E6%8B%B3%E5%AE%9E%E6%88%98">cut/sort/uniq 那批</a> 一样,真正的威力在管道组合里。</p><h2 id="head:取开头"><a href="#head:取开头" class="headerlink" title="head:取开头"></a>head:取开头</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">head</span> /var/log/app.log</span><br><span class="line"><span class="comment"># 默认输出文件前 10 行</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">head</span> -n 50 /var/log/app.log</span><br><span class="line"><span class="comment"># -n:指定行数,这里取前 50 行</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">head</span> -c 100 data.bin</span><br><span class="line"><span class="comment"># -c:按字节数取而不是按行,这里取前 100 个字节</span></span><br><span class="line"><span class="comment"># 检查一个大文件/二进制文件的开头是什么内容时有用</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">ls</span> -t | <span class="built_in">head</span> -n 5</span><br><span class="line"><span class="comment"># 管道用法:ls -t 按修改时间排序,head 取最新的 5 个文件名</span></span><br><span class="line"><span class="comment"># "取排序结果的前 N 个"是 head 在管道里最常见的角色</span></span><br></pre></td></tr></table></figure><h2 id="tail:取结尾,和一个特殊语法"><a href="#tail:取结尾,和一个特殊语法" class="headerlink" title="tail:取结尾,和一个特殊语法"></a>tail:取结尾,和一个特殊语法</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">tail</span> /var/log/app.log</span><br><span class="line"><span class="comment"># 默认输出文件最后 10 行</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">tail</span> -n 200 /var/log/app.log</span><br><span class="line"><span class="comment"># 最后 200 行——排查刚发生的问题,第一反应通常就是这条</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">tail</span> -n +5 data.csv</span><br><span class="line"><span class="comment"># 注意 +5 带加号,语义完全不同:从第 5 行开始一直输出到文件末尾</span></span><br><span class="line"><span class="comment"># 最常见的用途是跳过文件开头的表头/说明行(+2 就是跳过第一行表头)</span></span><br></pre></td></tr></table></figure><p><code>-n +N</code> 这个加号语法容易被忽略:<code>tail -n 5</code> 是”最后 5 行”,<code>tail -n +5</code> 是”从第 5 行到结尾”——一个从后往前数,一个从前往后跳,方向完全相反。</p><h2 id="head-tail-组合:取中间任意一段"><a href="#head-tail-组合:取中间任意一段" class="headerlink" title="head + tail 组合:取中间任意一段"></a>head + tail 组合:取中间任意一段</h2><p>两个命令一接,就能精确取出文件中间的任意行段:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">head</span> -n 12 access.log | <span class="built_in">tail</span> -n 5</span><br><span class="line"><span class="comment"># 先取前 12 行,再从这 12 行里取最后 5 行——最终得到第 8 到第 12 行</span></span><br><span class="line"><span class="comment"># 公式:取第 M 到第 N 行 = head -n N | tail -n $((N-M+1))</span></span><br></pre></td></tr></table></figure><p>典型场景:报错信息在日志第 3800 行附近,<code>head -n 3820 app.log | tail -n 40</code> 直接把上下文捞出来,不用打开几十万行的大文件翻。</p><h2 id="tail-f:实时跟踪日志"><a href="#tail-f:实时跟踪日志" class="headerlink" title="tail -f:实时跟踪日志"></a>tail -f:实时跟踪日志</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">tail</span> -f /var/log/app.log</span><br><span class="line"><span class="comment"># -f:follow,输出最后 10 行后不退出,持续等待并输出新追加的内容</span></span><br><span class="line"><span class="comment"># 盯着服务日志看请求进来的样子,就是它</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">tail</span> -f /var/log/app.log | grep --line-buffered ERROR</span><br><span class="line"><span class="comment"># 配合 grep 只看错误行</span></span><br><span class="line"><span class="comment"># --line-buffered:让 grep 逐行刷新输出——管道里 grep 默认会攒缓冲,</span></span><br><span class="line"><span class="comment"># 不加这个参数会感觉"日志卡住了很久突然涌出一批"</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">tail</span> -f access.log error.log</span><br><span class="line"><span class="comment"># 同时跟踪多个文件,输出里会标注每段来自哪个文件</span></span><br></pre></td></tr></table></figure><h2 id="tail-F:日志轮转的正解"><a href="#tail-F:日志轮转的正解" class="headerlink" title="tail -F:日志轮转的正解"></a>tail -F:日志轮转的正解</h2><p>回到开头的问题。<code>tail -f</code> 打开文件后,盯住的是<strong>打开那一刻的文件本体</strong>(准确说是它的 inode,这个概念在 <a href="/ln%E8%BD%AF%E7%A1%AC%E9%93%BE%E6%8E%A5%E5%AE%9E%E6%88%98-inode%E4%B8%8E%E7%9B%B8%E5%AF%B9%E8%B7%AF%E5%BE%84%E5%9D%91">ln 那篇</a> 讲过)。而日志轮转(logrotate)的动作通常是:把 <code>app.log</code> 改名成 <code>app.log.1</code>,再新建一个空的 <code>app.log</code> 继续写。</p><p>于是 <code>-f</code> 的困境出现了:它还忠实地盯着旧 inode——也就是改名后的 <code>app.log.1</code>,那个文件再也不会有新内容了,屏幕自然一片死寂;新日志全写进了新建的 <code>app.log</code>,没人看。</p><p><code>-F</code> 就是为这个场景设计的:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">tail</span> -F /var/log/app.log</span><br><span class="line"><span class="comment"># -F:在 -f 的基础上,持续检查文件名对应的 inode 有没有变化</span></span><br><span class="line"><span class="comment"># 检测到轮转(inode 变了)就关掉旧文件、重新打开新文件,无缝接上</span></span><br><span class="line"><span class="comment"># 文件暂时不存在也不报错退出,而是等它出现——服务重建日志文件的间隙也能扛过去</span></span><br></pre></td></tr></table></figure><p>结论很简单:<strong>只要盯的日志会轮转(生产环境的日志几乎都会),一律用大写 F</strong>。多敲一个大写字母,省掉半夜”日志为什么不动了”的惊魂。</p><h2 id="常用参数速查表"><a href="#常用参数速查表" class="headerlink" title="常用参数速查表"></a>常用参数速查表</h2><table><thead><tr><th>命令/参数</th><th>作用</th></tr></thead><tbody><tr><td><code>head 文件</code> / <code>tail 文件</code></td><td>取开头 / 结尾,默认各 10 行</td></tr><tr><td><code>head -n N</code> / <code>tail -n N</code></td><td>指定行数</td></tr><tr><td><code>head -c N</code></td><td>按字节取开头 N 字节</td></tr><tr><td><code>tail -n +N</code></td><td>从第 N 行取到结尾(+2 跳过表头)</td></tr><tr><td><code>head -n N | tail -n K</code></td><td>取第 N-K+1 到第 N 行的中间段</td></tr><tr><td><code>tail -f</code></td><td>实时跟踪新增内容</td></tr><tr><td><code>tail -f | grep --line-buffered</code></td><td>实时过滤,避免 grep 缓冲攒批</td></tr><tr><td><code>tail -F</code></td><td>跟踪且自动处理日志轮转,生产日志一律用它</td></tr></tbody></table><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p><code>head</code>/<code>tail</code> 五分钟就能全部学完,值得刻进记忆的是三个点:<code>-n +N</code> 的加号语义(从第 N 行到结尾)、<code>head | tail</code> 取中间段的组合、以及 <code>-F</code> 对 <code>-f</code> 的完全上位替代(只要日志会轮转)。最后这个坑踩过一次就再也忘不掉——不如直接从今天开始把 <code>-F</code> 设成肌肉记忆。</p>]]>
</content>
<id>https://blog.codewater.top/head%E4%B8%8Etail%E5%AE%9E%E6%88%98-%E5%8F%96%E8%A1%8C%E4%B8%8Etail-F%E8%BD%AE%E8%BD%AC%E5%9D%91/</id>
<link href="https://blog.codewater.top/head%E4%B8%8Etail%E5%AE%9E%E6%88%98-%E5%8F%96%E8%A1%8C%E4%B8%8Etail-F%E8%BD%AE%E8%BD%AC%E5%9D%91/"/>
<published>2026-07-18T08:31:25.000Z</published>
<summary>head/tail 取头取尾、-n +N 从第 N 行取到结尾、head+tail 取中间段,以及 tail -F 治日志轮转。</summary>
<title>head 与 tail 实战:取头取尾、取中间段与 tail -F 轮转坑</title>
<updated>2026-07-18T08:31:25.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="命令行" scheme="https://blog.codewater.top/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
<category term="文件系统" scheme="https://blog.codewater.top/tags/%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F/"/>
<content>
<![CDATA[<p><code>ln -s</code> 建的软链接,挪个位置就失效;硬链接和软链接的区别,问十个人有八个说不清。这两个问题的根源是同一个概念——inode。把它理解了,<code>ln</code> 的所有行为都变得顺理成章。</p><span id="more"></span><p>这是 Linux 命令系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>。<code>ln</code>(link)用来给文件创建链接——同一份内容的另一个访问入口,分硬链接和软链接(符号链接)两种,行为差别很大。</p><h2 id="inode:文件名和文件内容是分开存的"><a href="#inode:文件名和文件内容是分开存的" class="headerlink" title="inode:文件名和文件内容是分开存的"></a>inode:文件名和文件内容是分开存的</h2><p>文件系统里,一个文件其实是两部分:<strong>inode</strong> 存文件的实际内容和元数据(大小、权限、时间戳),<strong>目录里的文件名</strong>只是一个指向 inode 的入口。名字和内容是分离的——这就是理解链接的全部前提。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ls</span> -i a.txt</span><br><span class="line"><span class="comment"># -i:显示文件的 inode 编号</span></span><br><span class="line"><span class="comment"># 83502402 a.txt</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">ls</span> -l a.txt</span><br><span class="line"><span class="comment"># -rw-r--r-- 2 baizhe staff 6 ...</span></span><br><span class="line"><span class="comment"># 第二列的数字就是"链接数"——有多少个名字指向这个 inode</span></span><br><span class="line"><span class="comment"># ls/stat 那篇文章里没展开的这一列,谜底在这</span></span><br></pre></td></tr></table></figure><p><a href="/ls-stat-du-df%E6%96%87%E4%BB%B6%E4%BF%A1%E6%81%AF%E4%B8%8E%E7%A3%81%E7%9B%98%E5%8D%A0%E7%94%A8%E5%AE%9E%E6%88%98">stat 那篇</a> 输出里的 Inode 字段、<code>ls -l</code> 的第二列,说的都是这套机制。</p><h2 id="硬链接:同一个-inode-的多个名字"><a href="#硬链接:同一个-inode-的多个名字" class="headerlink" title="硬链接:同一个 inode 的多个名字"></a>硬链接:同一个 inode 的多个名字</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ln</span> a.txt b.txt</span><br><span class="line"><span class="comment"># 语法:ln 已有文件 新链接名——目标在前,链接名在后</span></span><br><span class="line"><span class="comment"># b.txt 是指向同一个 inode 的另一个名字,不是拷贝</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">ls</span> -li a.txt b.txt</span><br><span class="line"><span class="comment"># 83502402 -rw-r--r-- 2 ... a.txt</span></span><br><span class="line"><span class="comment"># 83502402 -rw-r--r-- 2 ... b.txt</span></span><br><span class="line"><span class="comment"># inode 编号完全相同,链接数都变成了 2——两个名字,一份内容</span></span><br></pre></td></tr></table></figure><p>因为两个名字完全平等地指向同一份内容,”删除”的语义也变了:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">rm</span> a.txt</span><br><span class="line"><span class="built_in">cat</span> b.txt</span><br><span class="line"><span class="comment"># 输出 hello——内容还在!</span></span><br><span class="line"><span class="comment"># rm 删的从来不是文件内容,只是目录里的一个名字(链接数减 1)</span></span><br><span class="line"><span class="comment"># 链接数归零、且没有进程持有句柄时,内容才真正被释放</span></span><br></pre></td></tr></table></figure><p>这和 <a href="/ss-lsof%E6%B7%B1%E5%85%A5-TCP%E8%BF%9E%E6%8E%A5%E7%8A%B6%E6%80%81%E4%B8%8E%E5%B7%B2%E5%88%A0%E9%99%A4%E6%96%87%E4%BB%B6%E5%8D%A0%E7%A3%81%E7%9B%98">lsof 那篇</a> 讲的”rm 掉的文件还占着磁盘”是同一个原理——名字没了不等于内容没了。</p><p>硬链接有两个硬性限制:<strong>不能跨文件系统</strong>(inode 编号只在单个文件系统内有意义,另一块盘上无从指起),<strong>不能对目录建</strong>(会造成目录树成环)。要跨盘、要链目录,就得用软链接。</p><h2 id="软链接:存着路径的独立小文件"><a href="#软链接:存着路径的独立小文件" class="headerlink" title="软链接:存着路径的独立小文件"></a>软链接:存着路径的独立小文件</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ln</span> -s a.txt link.txt</span><br><span class="line"><span class="comment"># -s:symbolic,创建软链接(符号链接)</span></span><br><span class="line"><span class="comment"># link.txt 是一个独立的小文件,有自己的 inode,内容就是一串路径字符串</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">ls</span> -l link.txt</span><br><span class="line"><span class="comment"># lrwxr-xr-x ... link.txt -> a.txt</span></span><br><span class="line"><span class="comment"># 权限位开头的 l 表示这是软链接,-> 显示它指向哪里</span></span><br></pre></td></tr></table></figure><p>软链接本质是”存着一个路径的文件”,访问它时系统顺着路径再找真正的目标。所以它没有硬链接的限制——可以跨文件系统、可以指向目录——代价是多了一层间接:目标没了,链接就悬空了:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">rm</span> a.txt</span><br><span class="line"><span class="built_in">cat</span> link.txt</span><br><span class="line"><span class="comment"># No such file or directory——目标被删,软链接变成"悬空链接"(dangling)</span></span><br><span class="line"><span class="comment"># 链接文件本身还在,只是指过去扑了个空;硬链接不存在这个问题</span></span><br></pre></td></tr></table></figure><h2 id="软链接最大的坑:相对路径"><a href="#软链接最大的坑:相对路径" class="headerlink" title="软链接最大的坑:相对路径"></a>软链接最大的坑:相对路径</h2><p>软链接里存的路径如果是相对的,<strong>它相对的是”链接所在的目录”,不是你执行 ln 命令时所在的目录</strong>。这是软链接失效的头号原因:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 当前目录下有 original.txt,想在子目录 sub/ 里建个链接指向它:</span></span><br><span class="line"><span class="built_in">ln</span> -s original.txt sub/link2.txt</span><br><span class="line"><span class="comment"># 直觉上没问题——original.txt 就在当前目录啊</span></span><br><span class="line"><span class="comment"># 实际上悬空了:链接存的字符串是 "original.txt",</span></span><br><span class="line"><span class="comment"># 解析时相对的是链接所在的 sub/ 目录,即 sub/original.txt——不存在</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">cat</span> sub/link2.txt</span><br><span class="line"><span class="comment"># No such file or directory</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">readlink</span> sub/link2.txt</span><br><span class="line"><span class="comment"># 输出 original.txt——readlink 显示链接里存的原始路径,验证问题的最快方法</span></span><br></pre></td></tr></table></figure><p>两种正确写法:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ln</span> -s /full/path/to/original.txt sub/link2.txt</span><br><span class="line"><span class="comment"># 解法一:目标写绝对路径,链接放哪都不会歪——脚本里建链接推荐这种</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">ln</span> -s ../original.txt sub/link2.txt</span><br><span class="line"><span class="comment"># 解法二:按"从链接所在目录出发"来写相对路径</span></span><br><span class="line"><span class="comment"># 相对路径的好处是整个目录树挪走后链接关系依然成立</span></span><br></pre></td></tr></table></figure><h2 id="软链接的两个典型场景"><a href="#软链接的两个典型场景" class="headerlink" title="软链接的两个典型场景"></a>软链接的两个典型场景</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ln</span> -s /opt/app/releases/v2.3.1 /opt/app/current</span><br><span class="line"><span class="comment"># 版本切换:程序永远访问 current,发新版只需要把链接指向新目录</span></span><br><span class="line"><span class="comment"># 切换是原子的、回滚只要把链接指回去——大量部署工具的标准做法</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">mv</span> /var/lib/docker /data/docker && <span class="built_in">ln</span> -s /data/docker /var/lib/docker</span><br><span class="line"><span class="comment"># 腾磁盘空间:把占地大的目录挪到空间充裕的盘,原位置留个软链接</span></span><br><span class="line"><span class="comment"># 应用无感知,路径照旧能访问</span></span><br></pre></td></tr></table></figure><p>硬链接的日常场景少一些,最常见的是备份工具的增量快照——同一份没变过的文件在多个快照目录里用硬链接共享,几十份”全量备份”实际只占一份多的空间。</p><h2 id="常用操作速查表"><a href="#常用操作速查表" class="headerlink" title="常用操作速查表"></a>常用操作速查表</h2><table><thead><tr><th>命令/概念</th><th>说明</th></tr></thead><tbody><tr><td><code>ln 目标 链接名</code></td><td>硬链接:同 inode 的另一个名字,语法目标在前</td></tr><tr><td><code>ln -s 目标 链接名</code></td><td>软链接:存路径的独立文件,可跨盘、可链目录</td></tr><tr><td><code>ls -i</code> / <code>ls -l</code> 第二列</td><td>查看 inode 编号 / 指向该 inode 的链接数</td></tr><tr><td>硬链接删除行为</td><td>rm 只删名字,链接数归零内容才释放</td></tr><tr><td>软链接删除行为</td><td>删目标后链接悬空;删链接不影响目标</td></tr><tr><td>硬链接限制</td><td>不能跨文件系统、不能对目录</td></tr><tr><td>软链接相对路径</td><td>相对于链接所在目录解析,不是执行命令的目录</td></tr><tr><td><code>readlink 链接</code></td><td>查看链接里存的原始路径,排查悬空第一步</td></tr><tr><td><code>ls -l</code> 开头的 <code>l</code></td><td>权限位首字母 l 即软链接,<code>-></code> 显示指向</td></tr></tbody></table><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p>一句话总结两种链接:硬链接是”同一个 inode 的多个平等名字”,软链接是”存着路径的独立小文件”。删除行为、跨盘限制、悬空问题,全都能从这两句话推出来。日常用得最多的是软链接的版本切换和目录搬迁,记住相对路径那个坑(相对于链接所在目录),基本就不会再踩雷了。</p>]]>
</content>
<id>https://blog.codewater.top/ln%E8%BD%AF%E7%A1%AC%E9%93%BE%E6%8E%A5%E5%AE%9E%E6%88%98-inode%E4%B8%8E%E7%9B%B8%E5%AF%B9%E8%B7%AF%E5%BE%84%E5%9D%91/</id>
<link href="https://blog.codewater.top/ln%E8%BD%AF%E7%A1%AC%E9%93%BE%E6%8E%A5%E5%AE%9E%E6%88%98-inode%E4%B8%8E%E7%9B%B8%E5%AF%B9%E8%B7%AF%E5%BE%84%E5%9D%91/"/>
<published>2026-07-17T09:43:52.000Z</published>
<summary>ln 硬链接和软链接差在哪:inode 原理、删除行为的区别、软链接相对路径的坑一篇讲清。</summary>
<title>ln 软硬链接实战:inode、删除行为与相对路径坑</title>
<updated>2026-07-17T09:43:52.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="crontab" scheme="https://blog.codewater.top/tags/crontab/"/>
<category term="运维" scheme="https://blog.codewater.top/tags/%E8%BF%90%E7%BB%B4/"/>
<content>
<![CDATA[<p>脚本手动跑得好好的,放进 crontab 就是不执行——没有报错、没有输出,像被吞掉了一样。这是 crontab 最经典的翻车现场,九成指向同一个原因(后面讲),但先把 crontab 本身理顺。</p><span id="more"></span><p>这是 Linux 命令系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>。<code>cron</code> 是类 Unix 系统的定时任务守护进程,<code>crontab</code> 是管理它的命令——每个用户有一份自己的任务表,按设定的时间自动执行命令。</p><h2 id="基本操作:-e、-l,以及要避开的-r"><a href="#基本操作:-e、-l,以及要避开的-r" class="headerlink" title="基本操作:-e、-l,以及要避开的 -r"></a>基本操作:-e、-l,以及要避开的 -r</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">crontab -e</span><br><span class="line"><span class="comment"># 编辑当前用户的任务表,用 $EDITOR 打开,保存退出即生效</span></span><br><span class="line"><span class="comment"># 每个用户一份独立的 crontab,互不干扰</span></span><br><span class="line"></span><br><span class="line">crontab -l</span><br><span class="line"><span class="comment"># 列出当前用户的所有定时任务</span></span><br><span class="line"></span><br><span class="line">crontab -r</span><br><span class="line"><span class="comment"># ⚠️ 删除当前用户的整个任务表——没有任何确认提示,敲下回车全没了</span></span><br><span class="line"><span class="comment"># -r 和 -e 在键盘上挨着,手滑的代价是全部任务瞬间蒸发</span></span><br><span class="line"><span class="comment"># 建议:永远用 -e 进去删掉某一行,别用 -r</span></span><br></pre></td></tr></table></figure><p>被 <code>-r</code> 清空且没有备份的话,任务表找不回来。有个简单的保险:定期 <code>crontab -l > ~/crontab.bak</code> 备份一份。</p><h2 id="五个时间字段:分-时-日-月-周"><a href="#五个时间字段:分-时-日-月-周" class="headerlink" title="五个时间字段:分 时 日 月 周"></a>五个时间字段:分 时 日 月 周</h2><p>crontab 每行由五个时间字段加一条命令组成,字段顺序固定:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">┌───────── 分钟 (0-59)</span><br><span class="line">│ ┌─────── 小时 (0-23)</span><br><span class="line">│ │ ┌───── 日 (1-31)</span><br><span class="line">│ │ │ ┌─── 月 (1-12)</span><br><span class="line">│ │ │ │ ┌─ 周几 (0-7,0 和 7 都是周日)</span><br><span class="line">│ │ │ │ │</span><br><span class="line">* * * * * 要执行的命令</span><br></pre></td></tr></table></figure><p>四种常用写法,组合起来能表达绝大多数调度需求:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">0 3 * * * /opt/scripts/backup.sh</span><br><span class="line"><span class="comment"># 每天凌晨 3:00 执行——* 表示"每一个",分钟填 0、小时填 3,其余全通配</span></span><br><span class="line"></span><br><span class="line">*/5 * * * * /opt/scripts/check.sh</span><br><span class="line"><span class="comment"># 每 5 分钟执行一次——*/N 表示"每隔 N"</span></span><br><span class="line"></span><br><span class="line">0 9 * * 1-5 /opt/scripts/report.sh</span><br><span class="line"><span class="comment"># 工作日(周一到周五)每天 9:00——N-M 表示连续范围</span></span><br><span class="line"></span><br><span class="line">0 8 1,15 * * /opt/scripts/remind.sh</span><br><span class="line"><span class="comment"># 每月 1 号和 15 号的 8:00——N,M 表示离散的几个值</span></span><br></pre></td></tr></table></figure><p>还有一组快捷写法,替代整个五字段:<code>@daily</code>(每天零点)、<code>@hourly</code>、<code>@weekly</code>、<code>@monthly</code>,以及比较特殊的 <code>@reboot</code>(系统启动时执行一次)。</p><h2 id="任务不执行的头号原因:cron-的环境变量是极简的"><a href="#任务不执行的头号原因:cron-的环境变量是极简的" class="headerlink" title="任务不执行的头号原因:cron 的环境变量是极简的"></a>任务不执行的头号原因:cron 的环境变量是极简的</h2><p>回到开头的问题。cron 执行任务时<strong>不加载你的 shell 配置</strong>——<code>.bashrc</code>、<code>.zshrc</code>、<code>.profile</code> 统统不读,<code>PATH</code> 通常只有 <code>/usr/bin:/bin</code> 这么两截。你手动跑脚本没问题,是因为交互 shell 里 PATH 齐全;cron 里同一条命令找不到可执行文件,就静默失败了。</p><p>两种解法,任选或同时用:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">which</span> node</span><br><span class="line"><span class="comment"># 先用 which 查出命令的绝对路径,比如 /usr/local/bin/node</span></span><br><span class="line"></span><br><span class="line">0 3 * * * /usr/local/bin/node /opt/app/task.js</span><br><span class="line"><span class="comment"># 解法一:crontab 里所有命令一律写绝对路径,包括脚本里调用的命令</span></span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">SHELL=/bin/bash</span><br><span class="line">PATH=/usr/local/bin:/usr/bin:/bin</span><br><span class="line">0 3 * * * node /opt/app/task.js</span><br><span class="line"><span class="comment"># 解法二:在 crontab 文件顶部显式声明 PATH 和 SHELL</span></span><br><span class="line"><span class="comment"># 声明之后,下面所有任务行都在这个环境下执行</span></span><br></pre></td></tr></table></figure><h2 id="输出去哪了:重定向到日志"><a href="#输出去哪了:重定向到日志" class="headerlink" title="输出去哪了:重定向到日志"></a>输出去哪了:重定向到日志</h2><p>任务的输出(stdout 和 stderr)默认会尝试通过本地邮件发给用户——绝大多数服务器根本没配邮件服务,输出就这么无声消失了,出错也看不到错误信息。标准做法是自己重定向:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1</span><br><span class="line"><span class="comment"># >> 追加写入日志文件(> 是覆盖写,日志要用追加)</span></span><br><span class="line"><span class="comment"># 2>&1 拆开看:程序有两条输出通道,1 是标准输出(正常输出),2 是标准错误(报错信息)</span></span><br><span class="line"><span class="comment"># >> 只接管了通道 1;"2>&1" 的意思是"让通道 2 也指向通道 1 当前指向的地方"</span></span><br><span class="line"><span class="comment"># 合起来:正常输出和报错都进这个日志文件——排查任务失败时,报错信息才是最想要的那部分</span></span><br><span class="line"><span class="comment"># 顺序有讲究:2>&1 必须写在 >> 文件 的后面,先定好 1 去哪,2 才能跟对地方</span></span><br></pre></td></tr></table></figure><p>怀疑任务压根没被触发(而不是执行出错)时,查 cron 自己的日志:</p><p>cron 服务的名字随发行版不同:Debian/Ubuntu 系叫 <code>cron</code>,RHEL/CentOS 系叫 <code>crond</code>——先确认自己机器上是哪个,再查它的日志:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">systemctl list-units --<span class="built_in">type</span>=service | grep -E <span class="string">"cron"</span></span><br><span class="line"><span class="comment"># 确认本机的服务名到底是 cron 还是 crond</span></span><br><span class="line"></span><br><span class="line">journalctl -u cron -n 50</span><br><span class="line"><span class="comment"># 看 cron 服务最近 50 条日志,每次任务触发都有记录</span></span><br><span class="line"><span class="comment"># RHEL/CentOS 系把 cron 换成 crond;journalctl 的过滤用法在 systemctl 那篇讲过</span></span><br><span class="line"><span class="comment"># 没有 systemd 的老系统翻 /var/log/syslog 或 /var/log/cron</span></span><br></pre></td></tr></table></figure><p>日志里有触发记录但结果不对 → 执行环境问题(八成是上一节的 PATH);连触发记录都没有 → 时间字段写错了。</p><h2 id="一个专属坑:-需要转义"><a href="#一个专属坑:-需要转义" class="headerlink" title="一个专属坑:% 需要转义"></a>一个专属坑:% 需要转义</h2><p>crontab 的命令部分里,<code>%</code> 是特殊字符——未转义的 <code>%</code> 会被当成换行符,它之后的内容全部作为标准输入喂给命令(<code>man 5 crontab</code> 原文如此)。最常见的踩法是在文件名里用 <code>date</code> 生成日期:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">0 3 * * * tar -czf /backup/logs-$(<span class="built_in">date</span> +\%F).tar.gz -C /var/log . </span><br><span class="line"><span class="comment"># date +%F 输出 2026-07-17 这种格式,但 % 必须写成 \%</span></span><br><span class="line"><span class="comment"># 不转义的话命令会在 % 处被截断,任务安静地失败,非常难查</span></span><br></pre></td></tr></table></figure><h2 id="实战:每天凌晨增量备份"><a href="#实战:每天凌晨增量备份" class="headerlink" title="实战:每天凌晨增量备份"></a>实战:每天凌晨增量备份</h2><p>把前面的点串成一个真实任务——每天凌晨 3 点用 rsync 做增量备份,写日志、防 PATH 坑:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">PATH=/usr/local/bin:/usr/bin:/bin</span><br><span class="line">0 3 * * * rsync -a --delete /data/ /backup/data/ >> /var/log/backup.log 2>&1</span><br><span class="line"><span class="comment"># rsync 的 -a、--delete 参数含义见 rsync 那篇</span></span><br><span class="line"><span class="comment"># 顶部声明 PATH,输出进日志,时间字段"每天 3:00"</span></span><br></pre></td></tr></table></figure><p>需求再复杂一点——比如”上一次没跑完就别启动下一次””失败自动重试””依赖网络就绪”——crontab 就力不从心了,那是 systemd timer 的领域,配合 <a href="/systemctl-journalctl%E5%AE%9E%E6%88%98-%E6%9C%8D%E5%8A%A1%E7%AE%A1%E7%90%86%E4%B8%8E%E6%97%A5%E5%BF%97%E8%BF%87%E6%BB%A4">systemctl 那篇</a> 的 unit 文件一起用。日常的定时脚本,crontab 足够。</p><h2 id="常用操作速查表"><a href="#常用操作速查表" class="headerlink" title="常用操作速查表"></a>常用操作速查表</h2><table><thead><tr><th>操作/写法</th><th>作用</th></tr></thead><tbody><tr><td><code>crontab -e</code></td><td>编辑任务表,保存即生效</td></tr><tr><td><code>crontab -l</code></td><td>查看任务表;<code>crontab -l > 备份文件</code> 定期备份</td></tr><tr><td><code>crontab -r</code></td><td>无确认清空全部任务,慎用,建议永远用 -e 删行</td></tr><tr><td>五字段顺序</td><td>分 时 日 月 周</td></tr><tr><td><code>*/5</code> / <code>1-5</code> / <code>1,15</code></td><td>每隔 5 / 连续范围 / 离散值</td></tr><tr><td><code>@daily</code> / <code>@reboot</code></td><td>每天零点 / 开机执行一次</td></tr><tr><td>PATH 坑</td><td>cron 不读 shell 配置,命令写绝对路径或顶部声明 PATH</td></tr><tr><td><code>>> 日志 2>&1</code></td><td>输出重定向进日志,否则静默消失</td></tr><tr><td><code>journalctl -u cron</code></td><td>确认任务有没有被触发</td></tr><tr><td><code>\%</code></td><td>命令里的 % 必须转义,否则命令被截断</td></tr></tbody></table><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p>crontab 的语法五分钟就能学会,真正拉开差距的是那几个坑:环境变量极简(绝对路径/声明 PATH)、输出会消失(重定向日志)、<code>%</code> 要转义、<code>-r</code> 别碰。把这四条焊在肌肉记忆里,定时任务”静默失败”的排查时间能从几小时缩到几分钟。</p>]]>
</content>
<id>https://blog.codewater.top/crontab%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1%E5%AE%9E%E6%88%98-%E6%97%B6%E9%97%B4%E5%AD%97%E6%AE%B5%E4%B8%8E%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E5%9D%91/</id>
<link href="https://blog.codewater.top/crontab%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1%E5%AE%9E%E6%88%98-%E6%97%B6%E9%97%B4%E5%AD%97%E6%AE%B5%E4%B8%8E%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F%E5%9D%91/"/>
<published>2026-07-17T09:41:40.000Z</published>
<summary>crontab 五个时间字段怎么写、-e/-l 常用操作、任务不执行九成是 PATH 问题:定时任务实战。</summary>
<title>crontab 定时任务实战:时间字段、环境变量坑与日志排查</title>
<updated>2026-07-18T04:59:36.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="运维" scheme="https://blog.codewater.top/tags/%E8%BF%90%E7%BB%B4/"/>
<category term="网络" scheme="https://blog.codewater.top/tags/%E7%BD%91%E7%BB%9C/"/>
<content>
<![CDATA[<p><code>ss -s</code> 一看,几千个 TIME_WAIT 连接,要不要处理?删掉了几个 G 的大日志,<code>df</code> 显示磁盘却一点没释放,空间去哪了?这两个问题,一个考验对 <code>ss</code> 输出里 TCP 状态的理解,一个考验对 <code>lsof</code> “一切皆文件”哲学的运用——都超出了”查个端口占用”的基本盘。</p><span id="more"></span><p>这是 Linux 命令系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>。先一句话对齐两个工具是干什么的:<code>ss</code>(socket statistics)查看系统里所有网络连接的状态,是 <code>netstat</code> 的现代替代;<code>lsof</code>(list open files)列出进程打开的所有”文件”——Linux 里网络连接、管道也都算文件,所以它既能查端口也能查文件占用。两者查端口监听/占用的基本用法在 <a href="/%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%8E%92%E6%9F%A5%E6%9C%8D%E5%8A%A1%E9%97%AE%E9%A2%98%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4">服务器排查那篇</a> 讲过了,本篇不再重复,直接进深水区。</p><h2 id="ss-按状态统计和过滤"><a href="#ss-按状态统计和过滤" class="headerlink" title="ss 按状态统计和过滤"></a>ss 按状态统计和过滤</h2><p><code>ss</code>(socket statistics)除了列连接,还能按 TCP 状态统计和过滤——排查连接类问题时,先看全局分布再钻具体状态:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">ss -s</span><br><span class="line"><span class="comment"># -s:按状态汇总统计所有 socket——estab(已建立)、time-wait 等各有多少</span></span><br><span class="line"><span class="comment"># 一眼看出连接的整体健康状况,是排查的第一步</span></span><br><span class="line"></span><br><span class="line">ss -tn state time-wait</span><br><span class="line"><span class="comment"># -t:TCP;-n:不解析域名和服务名(快很多)</span></span><br><span class="line"><span class="comment"># state 后面跟状态名,只列出处于这个状态的连接</span></span><br><span class="line"></span><br><span class="line">ss -tn state close-wait</span><br><span class="line"><span class="comment"># 同理,专门看 close-wait 状态的连接——后文会讲为什么要特别盯这个</span></span><br><span class="line"></span><br><span class="line">ss -tnp state established <span class="string">'( dport = :443 )'</span></span><br><span class="line"><span class="comment"># 还能按端口过滤:只看目标端口 443 的已建立连接</span></span><br><span class="line"><span class="comment"># -p:显示是哪个进程持有这条连接(需要相应权限)</span></span><br></pre></td></tr></table></figure><h2 id="TIME-WAIT:正常现象,别急着动内核参数"><a href="#TIME-WAIT:正常现象,别急着动内核参数" class="headerlink" title="TIME_WAIT:正常现象,别急着动内核参数"></a>TIME_WAIT:正常现象,别急着动内核参数</h2><p>TCP 连接关闭时,<strong>主动发起关闭的一方</strong>会进入 TIME_WAIT 状态,等待一段固定时间(2 倍 MSL,通常总共几十秒到几分钟)后自动消失——这是 TCP 协议设计的一部分,用来确保最后的确认包送达、避免旧连接的残留数据包串进新连接。</p><p>所以看到一批 TIME_WAIT 不用慌:它们不占进程资源(连接已经关了),会自己过期,是协议在正常工作。数量大到几千上万,说明的也不是故障,而是”短连接建立和拆除得太频繁”——比如服务间调用没用连接池、HTTP 客户端没开 keep-alive。这时候正确的方向是改用长连接/连接池,从源头减少连接的建拆次数,而不是上来就调内核参数硬压。</p><h2 id="CLOSE-WAIT:这个才需要警惕"><a href="#CLOSE-WAIT:这个才需要警惕" class="headerlink" title="CLOSE_WAIT:这个才需要警惕"></a>CLOSE_WAIT:这个才需要警惕</h2><p>CLOSE_WAIT 的含义完全不同:<strong>对端已经关闭了连接,但本端的应用程序还没有调用 close() 把连接关掉</strong>。内核收到了对方的挥手、在等你的程序把连接收尾,你的程序却一直没动作——连接就卡在 CLOSE_WAIT。</p><p>零星几个是瞬时现象,但 CLOSE_WAIT <strong>持续堆积</strong>基本可以断定是应用代码的 bug:某个分支漏了关闭连接(常见于异常路径——请求出错提前 return,忘了释放连接)。特征也很好认:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">ss -tn state close-wait | <span class="built_in">wc</span> -l</span><br><span class="line"><span class="comment"># 隔一分钟跑一次,数量只涨不跌,就是泄漏没跑了</span></span><br><span class="line"></span><br><span class="line">ss -tnp state close-wait</span><br><span class="line"><span class="comment"># -p 看这些连接挂在哪个进程上,直接定位到该背锅的服务</span></span><br></pre></td></tr></table></figure><p>重启进程能把 CLOSE_WAIT 清零,但那只是止痛——代码不修,堆积会再来一遍。这是”监控指标能直接指向代码 bug”的少数场景之一,值得把 CLOSE_WAIT 数量放进告警。</p><h2 id="lsof:一切皆文件的排查方式"><a href="#lsof:一切皆文件的排查方式" class="headerlink" title="lsof:一切皆文件的排查方式"></a>lsof:一切皆文件的排查方式</h2><p>Linux 里普通文件、目录、网络连接、管道全是”文件”,<code>lsof</code>(list open files)因此成了万能反查工具——从进程查文件,或从文件反查进程:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">lsof -p 12345</span><br><span class="line"><span class="comment"># -p:列出指定进程打开的所有文件——配置读的是哪份、日志写到哪去了,一目了然</span></span><br><span class="line"></span><br><span class="line">lsof /var/log/app.log</span><br><span class="line"><span class="comment"># 参数直接给文件路径:反查谁在用这个文件</span></span><br><span class="line"></span><br><span class="line">lsof +D /mnt/data</span><br><span class="line"><span class="comment"># +D:反查谁在用这个目录(递归)——umount 报 "target is busy" 时,</span></span><br><span class="line"><span class="comment"># 用它找出占着挂载点不放的进程</span></span><br><span class="line"></span><br><span class="line">lsof -u deploy</span><br><span class="line"><span class="comment"># -u:列出指定用户打开的所有文件</span></span><br></pre></td></tr></table></figure><h2 id="lsof-L1:已删除但还占着磁盘的文件"><a href="#lsof-L1:已删除但还占着磁盘的文件" class="headerlink" title="lsof +L1:已删除但还占着磁盘的文件"></a>lsof +L1:已删除但还占着磁盘的文件</h2><p>回收 <a href="/ls-stat-du-df%E6%96%87%E4%BB%B6%E4%BF%A1%E6%81%AF%E4%B8%8E%E7%A3%81%E7%9B%98%E5%8D%A0%E7%94%A8%E5%AE%9E%E6%88%98">ls/du/df 那篇</a> 埋的伏笔——<code>df</code> 显示磁盘快满,<code>du</code> 把目录加起来却对不上,差的那块空间在哪?</p><p>要理解这个现象,先把三个东西分开:<strong>文件内容</strong>(磁盘上的数据块,真正占空间的是它)、<strong>目录里的文件名</strong>(指向内容的入口)、<strong>进程的文件句柄</strong>(进程打开文件后拿到的访问通道)。关键规则是:<strong>内容被释放的条件,是”没有任何名字指向它”且”没有任何进程打开着它”——两个条件要同时满足</strong>。</p><p><code>rm</code> 只做了一件事:删掉目录里的名字。如果这时还有进程开着这个文件(典型场景:删了正在被服务写入的日志),第二个条件不满足,数据块就原地不动——于是出现了诡异的一幕:</p><ul><li><code>du</code> 是沿着目录树把文件加起来统计的,名字没了,它就统计不到这份内容</li><li><code>df</code> 统计的是文件系统层面数据块的实际占用,这份”无名内容”照样算在内</li></ul><p>你的理解方向是对的:<code>df</code> 显示的确实还包含着”删除前”的那份空间——因为那份内容根本没被删掉,被删掉的只是名字。<code>lsof +L1</code> 就是把这些”有内容、有句柄、没名字”的文件找出来:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">lsof +L1</span><br><span class="line"><span class="comment"># +L1:列出链接数小于 1 的打开文件——也就是"已删除但仍被进程占用"的文件</span></span><br><span class="line"><span class="comment"># 输出里 SIZE/OFF 列能看到文件多大,NAME 列会标注 (deleted)</span></span><br><span class="line"></span><br><span class="line">lsof +L1 /var/log</span><br><span class="line"><span class="comment"># 限定只查某个路径下的,范围小输出更干净</span></span><br></pre></td></tr></table></figure><p>找到之后有两种处理,原理不同但殊途同归:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">systemctl restart myapp</span><br><span class="line"><span class="comment"># 方式一:重启持有句柄的进程——句柄释放后,"两个条件"凑齐,内容被回收,空间回来</span></span><br><span class="line"><span class="comment"># 进程优雅退出的机制见 kill 信号那篇——systemctl stop 会先发 SIGTERM</span></span><br><span class="line"></span><br><span class="line">: > /proc/12345/fd/4</span><br><span class="line"><span class="comment"># 方式二(不能重启服务时):不动句柄,直接把内容清空</span></span><br><span class="line"><span class="comment"># /proc/PID/fd/ 目录下,内核把进程打开的每个文件句柄都暴露成一个入口,</span></span><br><span class="line"><span class="comment"># 编号 N 从 lsof 输出的 FD 列拿(比如 4w 就是 4)——文件在目录里的名字虽然没了,</span></span><br><span class="line"><span class="comment"># 但通过这个入口依然能访问到那份内容</span></span><br><span class="line"><span class="comment"># : 是 shell 的空命令(什么都不输出),> 是重定向——"把空输出写入这个文件",</span></span><br><span class="line"><span class="comment"># 效果就是把文件内容截断成 0 字节</span></span><br></pre></td></tr></table></figure><p>回答一个自然的疑问:方式二清空后句柄确实还开着,为什么空间能释放?因为<strong>占磁盘的是数据块,不是句柄</strong>——句柄只是访问通道,本身不占什么空间。内容被截断成 0 字节,数据块立刻归还给文件系统,<code>df</code> 马上就能看到下降;进程后续再写日志,从 0 开始占用新的块,服务全程不受影响。</p><p>预防比补救简单:要清空正在被写入的日志,用 <code>truncate -s 0 文件</code> 或 <code>: > 文件</code> 原地清空内容(名字和句柄都不动,只清数据块),别用 <code>rm</code>——<code>rm</code> 删的是名字、留下的恰恰是那份占空间的内容,方向完全反了。</p><h2 id="常用命令速查表"><a href="#常用命令速查表" class="headerlink" title="常用命令速查表"></a>常用命令速查表</h2><table><thead><tr><th>命令</th><th>作用</th></tr></thead><tbody><tr><td><code>ss -s</code></td><td>按状态汇总统计所有连接</td></tr><tr><td><code>ss -tn state 状态名</code></td><td>只看指定状态的 TCP 连接</td></tr><tr><td><code>ss -tnp state close-wait</code></td><td>查 CLOSE_WAIT 挂在哪个进程</td></tr><tr><td>TIME_WAIT 大量出现</td><td>短连接太频繁,上连接池/keep-alive,通常不是故障</td></tr><tr><td>CLOSE_WAIT 持续堆积</td><td>应用漏了 close(),修代码,重启只是止痛</td></tr><tr><td><code>lsof -p PID</code></td><td>进程打开了哪些文件</td></tr><tr><td><code>lsof 文件路径</code> / <code>+D 目录</code></td><td>反查谁在用这个文件/目录(umount busy 排查)</td></tr><tr><td><code>lsof +L1</code></td><td>已删除但仍被占用的文件,df/du 对不上时查它</td></tr><tr><td><code>: > /proc/PID/fd/N</code></td><td>不重启进程直接释放已删除文件的空间</td></tr><tr><td>清空活跃日志</td><td>用 <code>truncate -s 0</code> 或 <code>: ></code>,别用 rm</td></tr></tbody></table><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p><code>ss</code> 的进阶在于读懂状态:TIME_WAIT 是协议正常运转(多了优化连接复用),CLOSE_WAIT 堆积是代码 bug 的直接证据。<code>lsof</code> 的进阶在于反向思维:从文件反查进程,<code>+L1</code> 专治”删了文件磁盘不释放”。这两个工具吃透,网络连接和文件句柄两类”隐形”问题就都有了抓手。</p>]]>
</content>
<id>https://blog.codewater.top/ss-lsof%E6%B7%B1%E5%85%A5-TCP%E8%BF%9E%E6%8E%A5%E7%8A%B6%E6%80%81%E4%B8%8E%E5%B7%B2%E5%88%A0%E9%99%A4%E6%96%87%E4%BB%B6%E5%8D%A0%E7%A3%81%E7%9B%98/</id>
<link href="https://blog.codewater.top/ss-lsof%E6%B7%B1%E5%85%A5-TCP%E8%BF%9E%E6%8E%A5%E7%8A%B6%E6%80%81%E4%B8%8E%E5%B7%B2%E5%88%A0%E9%99%A4%E6%96%87%E4%BB%B6%E5%8D%A0%E7%A3%81%E7%9B%98/"/>
<published>2026-07-17T09:39:34.000Z</published>
<summary>ss 看 TCP 连接状态:TIME_WAIT/CLOSE_WAIT 说明什么;lsof 揪出已删除却还占磁盘的文件。</summary>
<title>ss 与 lsof 深入:TIME_WAIT、CLOSE_WAIT 与已删除还占磁盘的文件</title>
<updated>2026-07-18T04:46:01.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="命令行" scheme="https://blog.codewater.top/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
<category term="SSH" scheme="https://blog.codewater.top/tags/SSH/"/>
<content>
<![CDATA[<p>每次连服务器输一遍密码,IP 和端口记在备忘录里翻着敲——这套体验其实配置一次就能永久甩掉:密钥登录免掉密码,config 别名免掉记 IP,剩下的就是 <code>ssh myserver</code> 三个词连上任何机器。</p><span id="more"></span><p>这是 Linux 命令系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>。</p><h2 id="ssh-基本连接"><a href="#ssh-基本连接" class="headerlink" title="ssh 基本连接"></a>ssh 基本连接</h2><p><code>ssh</code> 是加密的远程登录协议和客户端命令,连上之后拿到的就是远程机器的 shell:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">ssh deploy@192.168.1.100</span><br><span class="line"><span class="comment"># 以 deploy 用户身份连接目标主机,默认端口 22</span></span><br><span class="line"></span><br><span class="line">ssh -p 2222 deploy@192.168.1.100</span><br><span class="line"><span class="comment"># -p:指定端口(小写 p)——注意这个细节,后面讲 scp 时它会变成一个坑</span></span><br></pre></td></tr></table></figure><h2 id="ssh-keygen:生成密钥对"><a href="#ssh-keygen:生成密钥对" class="headerlink" title="ssh-keygen:生成密钥对"></a>ssh-keygen:生成密钥对</h2><p>密钥登录的原理一句话:生成一对钥匙,<strong>公钥放到服务器上,私钥留在本地绝不外传</strong>;连接时客户端用私钥证明身份,服务器用公钥验证,全程不需要密码。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">ssh-keygen -t ed25519 -C <span class="string">"you@example.com"</span></span><br><span class="line"><span class="comment"># -t:密钥类型,ed25519 是当前 OpenSSH 的默认推荐——密钥短、速度快、安全性好</span></span><br><span class="line"><span class="comment"># -C:注释,通常写邮箱,方便在服务器上区分这是谁的哪把钥匙</span></span><br><span class="line"><span class="comment"># 一路回车用默认路径(~/.ssh/id_ed25519),会问要不要设 passphrase:</span></span><br><span class="line"><span class="comment"># 设了更安全(私钥文件被偷也用不了),个人机器图省事可以留空</span></span><br></pre></td></tr></table></figure><p>生成后 <code>~/.ssh/</code> 下多出两个文件:<code>id_ed25519</code> 是私钥(权限 600,谁都别给),<code>id_ed25519.pub</code> 是公钥(就是要放到服务器上的那个)。</p><p>需要管理多台机器、每台用独立密钥时(比如公司服务器一把、自己 VPS 一把、GitHub 再一把),用 <code>-f</code> 给每把钥匙指定自己的文件名,不然后生成的会覆盖先生成的:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">ssh-keygen -t ed25519 -C <span class="string">"you@example.com"</span> -f ~/.ssh/id_ed25519_work</span><br><span class="line"><span class="comment"># -f:指定密钥文件路径和名称,生成 id_ed25519_work(私钥)和</span></span><br><span class="line"><span class="comment"># id_ed25519_work.pub(公钥),不动默认的 id_ed25519</span></span><br><span class="line"></span><br><span class="line">ssh-keygen -t ed25519 -C <span class="string">"you@example.com"</span> -f ~/.ssh/id_ed25519_vps</span><br><span class="line"><span class="comment"># 再给自己的 VPS 单独生成一把,两把钥匙互不干扰</span></span><br></pre></td></tr></table></figure><p>多把钥匙怎么让 ssh 知道连哪台机器用哪把?靠的就是后面 <code>~/.ssh/config</code> 里的 <code>IdentityFile</code> 字段——每台服务器的配置段里指定自己的那把私钥。</p><h2 id="ssh-copy-id:把公钥装到服务器上"><a href="#ssh-copy-id:把公钥装到服务器上" class="headerlink" title="ssh-copy-id:把公钥装到服务器上"></a>ssh-copy-id:把公钥装到服务器上</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">ssh-copy-id deploy@192.168.1.100</span><br><span class="line"><span class="comment"># 把本地公钥追加到服务器的 ~/.ssh/authorized_keys 里</span></span><br><span class="line"><span class="comment"># 这一步还需要输一次密码——这是最后一次,装好之后就免密了</span></span><br><span class="line"></span><br><span class="line">ssh deploy@192.168.1.100</span><br><span class="line"><span class="comment"># 再连就直接进去了,不再询问密码</span></span><br></pre></td></tr></table></figure><p>没有 <code>ssh-copy-id</code> 的环境(比如从 Windows 连),手动做等价的事:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cat</span> ~/.ssh/id_ed25519.pub | ssh deploy@192.168.1.100 <span class="string">"mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"</span></span><br><span class="line"><span class="comment"># 把公钥内容追加到服务器的 authorized_keys 文件</span></span><br></pre></td></tr></table></figure><p>免密不生效时,第一个该查的是服务器端的目录权限——sshd 对权限要求很严格,太宽松会直接拒绝密钥登录:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">chmod</span> 700 ~/.ssh</span><br><span class="line"><span class="built_in">chmod</span> 600 ~/.ssh/authorized_keys</span><br><span class="line"><span class="comment"># ~/.ssh 必须 700、authorized_keys 必须 600(或更严)</span></span><br><span class="line"><span class="comment"># 权限位的含义见 chmod 那篇:700 = 只有属主能读写进入</span></span><br></pre></td></tr></table></figure><h2 id="ssh-config:给服务器起别名"><a href="#ssh-config:给服务器起别名" class="headerlink" title="~/.ssh/config:给服务器起别名"></a>~/.ssh/config:给服务器起别名</h2><p>密钥解决了密码,config 解决记 IP。在本地 <code>~/.ssh/config</code> 里给每台服务器写一段:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">Host myserver</span><br><span class="line"> HostName 192.168.1.100</span><br><span class="line"> User deploy</span><br><span class="line"> Port 2222</span><br><span class="line"> IdentityFile ~/.ssh/id_ed25519</span><br><span class="line"># Host:你给它起的别名,后面连接时用这个名字</span><br><span class="line"># HostName:真实 IP 或域名</span><br><span class="line"># User/Port:默认用户和端口,连接时就不用每次写</span><br><span class="line"># IdentityFile:指定用哪把私钥,只有一把钥匙时可以省略</span><br></pre></td></tr></table></figure><p>配好之后:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ssh myserver</span><br><span class="line"><span class="comment"># 等价于 ssh -p 2222 -i ~/.ssh/id_ed25519 deploy@192.168.1.100</span></span><br></pre></td></tr></table></figure><p>管理多台服务器时价值更明显——每台一段配置,名字起得有意义(<code>prod-web</code>、<code>test-db</code>),连哪台敲哪个名字,IP 和端口再也不用记。<code>scp</code>、<code>rsync</code> 也认这份配置,别名全程通用。</p><h2 id="scp:基于-SSH-传文件"><a href="#scp:基于-SSH-传文件" class="headerlink" title="scp:基于 SSH 传文件"></a>scp:基于 SSH 传文件</h2><p><code>scp</code> 走 SSH 通道在本地和远程之间拷贝文件,语法和 <code>cp</code> 一致——把其中一端写成 <code>用户@主机:路径</code> 就行:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">scp app.tar.gz myserver:/opt/deploy/</span><br><span class="line"><span class="comment"># 上传:本地文件 → 远程目录(这里直接用了 config 里的别名)</span></span><br><span class="line"></span><br><span class="line">scp myserver:/var/log/app.log ./</span><br><span class="line"><span class="comment"># 下载:远程文件 → 本地当前目录</span></span><br><span class="line"></span><br><span class="line">scp -r ./dist myserver:/opt/deploy/</span><br><span class="line"><span class="comment"># -r:整个目录递归拷贝</span></span><br></pre></td></tr></table></figure><p>一个必踩一次的坑:<strong>scp 指定端口用大写 -P</strong>,和 ssh 的小写 <code>-p</code> 不一样——因为 scp 的小写 <code>-p</code> 已经被”保留文件的修改时间和权限位”占用了(man 手册原文就是这么解释的):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">scp -P 2222 app.tar.gz deploy@192.168.1.100:/opt/deploy/</span><br><span class="line"><span class="comment"># -P:端口,大写——写成小写 -p 不会报错,但那是"保留时间戳"的意思,</span></span><br><span class="line"><span class="comment"># 端口会走默认 22,连接失败还不容易看出原因</span></span><br><span class="line"><span class="comment"># 用 ~/.ssh/config 配好别名可以彻底绕开这个坑:端口写在配置里</span></span><br></pre></td></tr></table></figure><p>偶尔传一两个文件 <code>scp</code> 最顺手;大目录、大文件、需要断点续传或增量同步的场景,换 <a href="/rsync%E5%A2%9E%E9%87%8F%E5%90%8C%E6%AD%A5%E5%AE%9E%E6%88%98-%E5%B8%B8%E7%94%A8%E5%8F%82%E6%95%B0%E4%B8%8E%E8%BF%9C%E7%A8%8B%E5%90%8C%E6%AD%A5">rsync</a> 更合适。</p><h2 id="常用命令速查表"><a href="#常用命令速查表" class="headerlink" title="常用命令速查表"></a>常用命令速查表</h2><table><thead><tr><th>命令</th><th>作用</th></tr></thead><tbody><tr><td><code>ssh user@host</code></td><td>远程登录,默认端口 22</td></tr><tr><td><code>ssh -p 端口</code></td><td>指定端口(小写 p)</td></tr><tr><td><code>ssh-keygen -t ed25519</code></td><td>生成密钥对,ed25519 是当前默认推荐</td></tr><tr><td><code>ssh-copy-id user@host</code></td><td>把公钥装到服务器,装完免密</td></tr><tr><td><code>chmod 700 ~/.ssh</code> + <code>600 authorized_keys</code></td><td>免密不生效先查这个权限</td></tr><tr><td><code>~/.ssh/config</code></td><td>别名配置,<code>ssh 别名</code> 直连,scp/rsync 通用</td></tr><tr><td><code>scp 文件 host:路径</code></td><td>上传;反过来写就是下载</td></tr><tr><td><code>scp -r</code></td><td>整目录递归拷贝</td></tr><tr><td><code>scp -P 端口</code></td><td>指定端口是大写 P,小写 p 是保留时间戳</td></tr></tbody></table><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p>一次性的配置换永久的顺手:<code>ssh-keygen</code> 生成 ed25519 密钥、<code>ssh-copy-id</code> 装到服务器、<code>~/.ssh/config</code> 起好别名,之后连任何机器都是 <code>ssh 名字</code> 一步到位。传文件 <code>scp</code> 顺手用,记住端口是大写 <code>-P</code>;要增量和断点续传,隔壁 <code>rsync</code> 伺候。</p>]]>
</content>
<id>https://blog.codewater.top/SSH%E5%85%8D%E5%AF%86%E7%99%BB%E5%BD%95%E4%B8%8Escp%E5%AE%9E%E6%88%98-%E5%AF%86%E9%92%A5%E9%85%8D%E7%BD%AE%E4%B8%8E%E4%BC%A0%E6%96%87%E4%BB%B6/</id>
<link href="https://blog.codewater.top/SSH%E5%85%8D%E5%AF%86%E7%99%BB%E5%BD%95%E4%B8%8Escp%E5%AE%9E%E6%88%98-%E5%AF%86%E9%92%A5%E9%85%8D%E7%BD%AE%E4%B8%8E%E4%BC%A0%E6%96%87%E4%BB%B6/"/>
<published>2026-07-14T12:48:27.000Z</published>
<summary>ssh-keygen 生成密钥、ssh-copy-id 免密登录、config 起别名、scp 传文件(-P 是大写)一篇配齐。</summary>
<title>SSH 免密登录与 scp 实战:密钥、config 别名与传文件</title>
<updated>2026-07-16T08:32:14.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="运维" scheme="https://blog.codewater.top/tags/%E8%BF%90%E7%BB%B4/"/>
<category term="systemd" scheme="https://blog.codewater.top/tags/systemd/"/>
<content>
<![CDATA[<p>服务器重启之后服务没起来,明明之前 <code>systemctl start</code> 跑得好好的——因为 <code>start</code> 和 <code>enable</code> 是两件独立的事,只做了前者,服务就只活到下一次重启为止。这类概念没对齐的坑,systemd 里还有几个。</p><span id="more"></span><p>这是 Linux 命令系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>;<a href="/%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%8E%92%E6%9F%A5%E6%9C%8D%E5%8A%A1%E9%97%AE%E9%A2%98%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4">服务器排查那篇</a> 列了 systemctl/journalctl 的基本命令,这篇讲背后的概念和进阶用法。</p><h2 id="systemd-和-unit:先对齐两个词"><a href="#systemd-和-unit:先对齐两个词" class="headerlink" title="systemd 和 unit:先对齐两个词"></a>systemd 和 unit:先对齐两个词</h2><p>systemd 是现代主流 Linux 发行版(Ubuntu 16.04+、CentOS 7+、Debian 8+)的服务管理器,负责开机按依赖顺序拉起服务、运行期间盯着它们的状态。它管理的最小单位叫 unit——服务只是其中最常见的一类(<code>.service</code>),此外还有定时器(<code>.timer</code>)、挂载点(<code>.mount</code>)等类型。日常说”管服务”,操作的就是 service 类型的 unit。</p><h2 id="start-管现在,enable-管开机:两件独立的事"><a href="#start-管现在,enable-管开机:两件独立的事" class="headerlink" title="start 管现在,enable 管开机:两件独立的事"></a>start 管现在,enable 管开机:两件独立的事</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">systemctl start nginx</span><br><span class="line"><span class="comment"># 现在就把服务跑起来——但只管这一次,重启机器后不会自动再起</span></span><br><span class="line"></span><br><span class="line">systemctl <span class="built_in">enable</span> nginx</span><br><span class="line"><span class="comment"># 设置开机自启——但不影响现在,跑这条命令时服务并不会立刻启动</span></span><br><span class="line"></span><br><span class="line">systemctl <span class="built_in">enable</span> --now nginx</span><br><span class="line"><span class="comment"># 两件事一步做完:设置开机自启,并且现在就启动</span></span><br><span class="line"></span><br><span class="line">systemctl <span class="built_in">disable</span> nginx</span><br><span class="line"><span class="comment"># 取消开机自启,同样不影响当前运行状态</span></span><br><span class="line"></span><br><span class="line">systemctl is-enabled nginx</span><br><span class="line"><span class="comment"># 查询是否已设置开机自启,输出 enabled / disabled</span></span><br></pre></td></tr></table></figure><p>两者完全正交:<code>start</code> 了没 <code>enable</code>,重启后服务消失;<code>enable</code> 了没 <code>start</code>,当前这会儿服务并没有跑。部署新服务的标准动作是 <code>enable --now</code>,一次把两头都管上。</p><p><code>stop</code> 的行为也值得知道:它默认先给服务进程发 SIGTERM 请求优雅退出,超时还没退才升级为 SIGKILL——正是 <a href="/kill%E4%BF%A1%E5%8F%B7%E6%9C%BA%E5%88%B6%E4%B8%8Enice%E4%BC%98%E5%85%88%E7%BA%A7%E5%AE%9E%E6%88%98">kill 信号那篇</a> 讲的”先礼后兵”流程,systemd 帮你做好了。</p><h2 id="systemctl-status-输出怎么读"><a href="#systemctl-status-输出怎么读" class="headerlink" title="systemctl status 输出怎么读"></a>systemctl status 输出怎么读</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">● nginx.service - A high performance web server</span><br><span class="line"> Loaded: loaded (/lib/systemd/system/nginx.service; enabled; ...)</span><br><span class="line"> Active: active (running) since Tue 2026-07-14 09:00:12 CST; 2h ago</span><br><span class="line"> Main PID: 1235 (nginx)</span><br><span class="line"> Memory: 12.4M</span><br><span class="line"># Loaded 行看两个信息:unit 文件在哪、有没有设置开机自启(enabled/disabled)</span><br><span class="line"># Active 行是当前状态:active (running) 正常;failed 是启动失败——</span><br><span class="line"># failed 时下面通常直接附着最近几条报错日志,排查从这里入手最快</span><br></pre></td></tr></table></figure><h2 id="unit-文件放哪、怎么改"><a href="#unit-文件放哪、怎么改" class="headerlink" title="unit 文件放哪、怎么改"></a>unit 文件放哪、怎么改</h2><p>unit 文件有两个主要目录,优先级不同:</p><ul><li><code>/lib/systemd/system/</code>(部分发行版是 <code>/usr/lib/systemd/system/</code>):包管理器安装软件时放的,<strong>别直接改</strong>——包升级会把改动覆盖掉</li><li><code>/etc/systemd/system/</code>:管理员自己的地盘,优先级更高——同名 unit 在两边都存在时,以这里的为准</li></ul><p>自己部署一个 Go 服务,最小可用的 unit 文件长这样:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># /etc/systemd/system/myapp.service</span></span><br><span class="line"><span class="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=My Go App</span><br><span class="line"><span class="attr">After</span>=network.target</span><br><span class="line"><span class="comment"># After:等网络就绪后再启动本服务</span></span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">ExecStart</span>=/opt/myapp/myapp</span><br><span class="line"><span class="comment"># 要运行的命令,必须是绝对路径</span></span><br><span class="line"><span class="attr">Restart</span>=<span class="literal">on</span>-failure</span><br><span class="line"><span class="comment"># 进程异常退出时自动拉起,正常 stop 不会触发</span></span><br><span class="line"><span class="attr">User</span>=myapp</span><br><span class="line"><span class="comment"># 用哪个用户身份运行,不写默认 root——生产环境建议专门建个低权限用户</span></span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=multi-user.target</span><br><span class="line"><span class="comment"># enable 时挂到哪个启动目标下,服务类 unit 基本都写这个</span></span><br></pre></td></tr></table></figure><p>改完 unit 文件(新建、修改都算)有一步必做:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">systemctl daemon-reload</span><br><span class="line"><span class="comment"># 让 systemd 重新读取 unit 文件——不跑这条,systemd 继续用内存里的旧配置,</span></span><br><span class="line"><span class="comment"># 你改的内容看起来"完全不生效",这是 unit 文件改了没反应的头号原因</span></span><br><span class="line"></span><br><span class="line">systemctl <span class="built_in">enable</span> --now myapp</span><br><span class="line"><span class="comment"># 然后正常启动</span></span><br></pre></td></tr></table></figure><h2 id="journalctl:按服务、时间、级别过滤日志"><a href="#journalctl:按服务、时间、级别过滤日志" class="headerlink" title="journalctl:按服务、时间、级别过滤日志"></a>journalctl:按服务、时间、级别过滤日志</h2><p>systemd 托管的服务,标准输出和标准错误都被 journald 收集,用 <code>journalctl</code> 查,不用满盘找日志文件:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">journalctl -u myapp</span><br><span class="line"><span class="comment"># -u:只看指定服务的日志,从最早开始分页显示</span></span><br><span class="line"></span><br><span class="line">journalctl -u myapp -f</span><br><span class="line"><span class="comment"># -f:实时跟踪新日志,等价于 tail -f 的体验</span></span><br><span class="line"></span><br><span class="line">journalctl -u myapp -n 100</span><br><span class="line"><span class="comment"># -n:只看最近 100 行</span></span><br><span class="line"></span><br><span class="line">journalctl -u myapp --since <span class="string">"2026-07-14 09:00"</span> --<span class="keyword">until</span> <span class="string">"2026-07-14 10:00"</span></span><br><span class="line"><span class="comment"># --since/--until:按时间段过滤,也接受 "1 hour ago"、"yesterday" 这类相对写法</span></span><br><span class="line"></span><br><span class="line">journalctl -u myapp -p err</span><br><span class="line"><span class="comment"># -p:按级别过滤,err 只看错误及以上(emerg/alert/crit/err)</span></span><br><span class="line"></span><br><span class="line">journalctl -u myapp -b</span><br><span class="line"><span class="comment"># -b:只看本次开机以来的日志——排查"重启后起不来"时配合 -p err 特别好用</span></span><br></pre></td></tr></table></figure><h2 id="常用命令速查表"><a href="#常用命令速查表" class="headerlink" title="常用命令速查表"></a>常用命令速查表</h2><table><thead><tr><th>命令</th><th>作用</th></tr></thead><tbody><tr><td><code>systemctl start/stop/restart 服务</code></td><td>启动 / 停止 / 重启(只管当下)</td></tr><tr><td><code>systemctl reload 服务</code></td><td>平滑重载配置,不中断服务(需服务支持)</td></tr><tr><td><code>systemctl enable --now 服务</code></td><td>设置开机自启并立刻启动</td></tr><tr><td><code>systemctl is-enabled 服务</code></td><td>查是否开机自启</td></tr><tr><td><code>systemctl status 服务</code></td><td>状态 + 最近日志,看 Loaded/Active 两行</td></tr><tr><td><code>systemctl daemon-reload</code></td><td>改过 unit 文件后必跑,否则改动不生效</td></tr><tr><td><code>journalctl -u 服务 -f</code></td><td>实时跟踪服务日志</td></tr><tr><td><code>journalctl -u 服务 --since "1 hour ago"</code></td><td>按时间过滤</td></tr><tr><td><code>journalctl -u 服务 -p err -b</code></td><td>本次开机以来的错误日志</td></tr></tbody></table><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p>systemd 的坑基本都是概念错位:<code>start</code> 和 <code>enable</code> 各管一头(记住 <code>enable --now</code>)、unit 文件改在 <code>/etc/systemd/system/</code> 且改完必须 <code>daemon-reload</code>。日志这边 <code>journalctl -u</code> 加上时间和级别过滤,比去 <code>/var/log</code> 翻文件体面得多。这套流程走顺了,自己部署的服务和发行版自带的服务就是同一套管理方式。</p>]]>
</content>
<id>https://blog.codewater.top/systemctl-journalctl%E5%AE%9E%E6%88%98-%E6%9C%8D%E5%8A%A1%E7%AE%A1%E7%90%86%E4%B8%8E%E6%97%A5%E5%BF%97%E8%BF%87%E6%BB%A4/</id>
<link href="https://blog.codewater.top/systemctl-journalctl%E5%AE%9E%E6%88%98-%E6%9C%8D%E5%8A%A1%E7%AE%A1%E7%90%86%E4%B8%8E%E6%97%A5%E5%BF%97%E8%BF%87%E6%BB%A4/"/>
<published>2026-07-14T12:46:22.000Z</published>
<summary>systemctl 的 enable 和 start 有什么区别、unit 文件放哪、journalctl 怎么按时间和级别过滤日志。</summary>
<title>systemctl 与 journalctl 实战:enable 和 start 的区别、unit 文件与日志过滤</title>
<updated>2026-07-14T12:46:22.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="进程" scheme="https://blog.codewater.top/tags/%E8%BF%9B%E7%A8%8B/"/>
<category term="监控" scheme="https://blog.codewater.top/tags/%E7%9B%91%E6%8E%A7/"/>
<content>
<![CDATA[<p><code>top</code> 显示 load average 12.5——这个数粗略代表”平均有多少个任务在抢 CPU 或者排队等资源”,越大说明机器越忙,12.5 看着已经很吓人了;可 CPU 使用率却只有 30%,这台机器到底是忙还是闲?<code>free</code> 显示 free 列只剩几百 M,服务却跑得好好的——内存到底够不够?监控命令谁都会敲,但输出里的指标读不懂,跑了也白跑。</p><span id="more"></span><p>这是 Linux 命令系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>;<a href="/%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%8E%92%E6%9F%A5%E6%9C%8D%E5%8A%A1%E9%97%AE%E9%A2%98%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4">服务器排查那篇</a> 给了 <code>ps</code>/<code>top</code>/<code>free</code> 的基本用法,这篇专讲输出怎么读懂。</p><h2 id="ps-aux-的-STAT-列:进程现在处于什么状态"><a href="#ps-aux-的-STAT-列:进程现在处于什么状态" class="headerlink" title="ps aux 的 STAT 列:进程现在处于什么状态"></a>ps aux 的 STAT 列:进程现在处于什么状态</h2><p><code>ps aux</code> 输出里的 STAT 列是最有信息量、也最少人看懂的一列。第一个字符是进程的主状态,就五种:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">R 运行中或在运行队列里排队等 CPU</span><br><span class="line">S 可中断睡眠——在等某个事件(网络数据、锁、定时器),绝大多数进程平时都是这个状态</span><br><span class="line">D 不可中断睡眠——通常是卡在磁盘 IO 上;这个状态的进程不响应任何信号,</span><br><span class="line"> 连 kill -9 都杀不动,只能等 IO 完成或者重启机器</span><br><span class="line">Z 僵尸进程——已经退出了,但父进程还没回收它的退出状态;</span><br><span class="line"> 杀僵尸本身没用(它已经死了),要处理的是它的父进程</span><br><span class="line">T 被暂停(收到 SIGTSTP/SIGSTOP,比如按了 Ctrl-Z)</span><br></pre></td></tr></table></figure><p>主状态后面可能还挂着几个后缀,常见的有这些:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">s 会话首进程(session leader)</span><br><span class="line">l 多线程进程</span><br><span class="line">+ 在前台进程组(占着终端)</span><br><span class="line">< 高优先级(nice 值为负,抢 CPU 更凶)</span><br><span class="line">N 低优先级(nice 值为正,会让着别的进程)</span><br><span class="line">L 有内存页被锁定在物理内存里,不允许换出(数据库进程常见)</span><br></pre></td></tr></table></figure><p>比如 <code>Ss</code> 读作”睡眠中的会话首进程”(守护进程的典型状态),<code>R+</code> 读作”正在前台运行”,<code>S<</code> 读作”睡眠中的高优先级进程”。<code><</code> 和 <code>N</code> 这两个后缀对应的 nice 优先级机制,在 <a href="/kill%E4%BF%A1%E5%8F%B7%E6%9C%BA%E5%88%B6%E4%B8%8Enice%E4%BC%98%E5%85%88%E7%BA%A7%E5%AE%9E%E6%88%98">kill 信号那篇</a> 的 nice/renice 部分讲过。</p><p>排查时有两个状态要特别敏感:<strong>一堆进程卡在 D 状态,基本可以断定磁盘 IO 出了问题</strong>(盘太慢、NFS 挂了);Z 状态零星一两个无害,大量出现说明父进程的代码有 bug(没有 wait 子进程)。<code>kill</code> 对这两种状态都无能为力,原因在 <a href="/kill%E4%BF%A1%E5%8F%B7%E6%9C%BA%E5%88%B6%E4%B8%8Enice%E4%BC%98%E5%85%88%E7%BA%A7%E5%AE%9E%E6%88%98">kill 信号那篇</a> 讲过——D 状态不响应信号,Z 已经死了没什么可杀的。</p><h2 id="VSZ-和-RSS:两个”内存占用”差在哪"><a href="#VSZ-和-RSS:两个”内存占用”差在哪" class="headerlink" title="VSZ 和 RSS:两个”内存占用”差在哪"></a>VSZ 和 RSS:两个”内存占用”差在哪</h2><p>还是看 <code>ps aux</code> 的输出——STAT 前面还有两列跟内存相关的数字,VSZ 和 RSS,多数人从来没细看过它们的区别:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">USER PID %CPU %MEM VSZ RSS ... COMMAND</span><br><span class="line">root 12345 15.2 8.4 2150432 342012 ... java -jar app.jar</span><br></pre></td></tr></table></figure><ul><li>VSZ(Virtual Set Size):进程的虚拟地址空间总大小,包括映射了但从没真正用过的内存、共享库等,通常大得吓人且没什么诊断价值</li><li>RSS(Resident Set Size):进程实际占用的物理内存</li></ul><p>两列的单位都是 KiB(1024 字节)——上面这行的 VSZ 2150432 KiB 约等于 2.05 GiB,RSS 342012 KiB 约 334 MiB。<code>ps</code> 不会帮你换算成人类可读单位,位数多的时候自己心算除两次 1024。</p><p>看一个进程”到底吃了多少内存”、判断有没有内存泄漏,盯 RSS——它持续上涨才是真的在吃内存;VSZ 大不代表任何问题。</p><h2 id="load-average:三个数字和核数一起看"><a href="#load-average:三个数字和核数一起看" class="headerlink" title="load average:三个数字和核数一起看"></a>load average:三个数字和核数一起看</h2><p><code>top</code> 第一行和 <code>uptime</code> 都会显示 load average,三个数字分别是最近 1 分钟、5 分钟、15 分钟的平均负载——粗略理解为”平均有多少个任务在使用或等待 CPU(Linux 还包括 D 状态等 IO 的进程)”。</p><p>关键在于:<strong>这个数字必须和 CPU 核数一起看</strong>。load 4.0 对单核机器是严重过载,对 16 核机器只是热身:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">nproc</span></span><br><span class="line"><span class="comment"># 查看 CPU 核数,load average 除以这个数才是"每核负载"</span></span><br><span class="line"><span class="comment"># 经验法则:每核负载持续接近或超过 1,机器就开始排队了</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">uptime</span></span><br><span class="line"><span class="comment"># load average: 12.50, 8.20, 3.10</span></span><br><span class="line"><span class="comment"># 三个数字还能看趋势:1 分钟 > 15 分钟说明负载正在上升(事情正在恶化),</span></span><br><span class="line"><span class="comment"># 反过来说明高峰已经过去,正在恢复</span></span><br></pre></td></tr></table></figure><p>开头那个问题的答案也在这:load 12.5 但 CPU 只用了 30%,多半是大量进程卡在 D 状态等 IO——load 算上了它们,CPU 却是闲的。这种”高 load 低 CPU”的组合,指向的是磁盘/网络存储瓶颈,不是 CPU 不够。</p><h2 id="top:界面输出怎么读"><a href="#top:界面输出怎么读" class="headerlink" title="top:界面输出怎么读"></a>top:界面输出怎么读</h2><p><code>top</code> 进入后是实时刷新界面,上半部分是系统摘要,下半部分是进程列表:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">top - 16:35:52 up 579 days, 22:14, 3 users, load average: 1.12, 1.23, 1.27</span><br><span class="line">Tasks: 230 total, 1 running, 229 sleeping, 0 stopped, 0 zombie</span><br><span class="line">%Cpu(s): 14.3 us, 12.2 sy, 0.0 ni, 71.0 id, 0.0 wa, 0.0 hi, 0.0 si, 2.4 st</span><br><span class="line">KiB Mem : 8008584 total, 680256 free, 5050156 used, 2278172 buff/cache</span><br><span class="line">KiB Swap: 0 total, 0 free, 0 used. 2205612 avail Mem</span><br><span class="line"></span><br><span class="line"> PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND</span><br><span class="line"> 1747 root 20 0 4044748 166192 612 S 102.0 2.1 98860:42 dockerd</span><br><span class="line"> 908 root 0 -20 11.3g 40368 4080 S 0.7 0.5 4186:36 etcd</span><br></pre></td></tr></table></figure><p>摘要区第三行 <code>%Cpu(s)</code> 是整机 CPU 时间的分布,几个字段的含义:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">us 用户态进程占的 CPU 时间(业务代码本身在跑)</span><br><span class="line">sy 内核态占的时间(系统调用、内核在干活)——sy 明显偏高说明系统调用太频繁</span><br><span class="line">ni 被调过 nice 值的进程占的时间</span><br><span class="line">id 空闲——越高越闲</span><br><span class="line">wa 等磁盘 IO 的时间——wa 高就是 IO 瓶颈的直接信号,和前面 D 状态的判断互相印证</span><br><span class="line">hi/si 硬件/软件中断占的时间,平时接近 0</span><br><span class="line">st 被宿主机偷走的时间——虚拟机/云服务器特有,st 持续偏高说明宿主机超卖了</span><br></pre></td></tr></table></figure><p>进程列表里的前几列:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">PR 内核眼中的调度优先级</span><br><span class="line">NI nice 值,就是 nice/renice 设置的那个数——etcd 那行的 -20 是最高优先级</span><br><span class="line">VIRT 虚拟内存,等同 ps 里的 VSZ,看看就好</span><br><span class="line">RES 实际物理内存,等同 ps 里的 RSS,看内存占用盯这列</span><br><span class="line">SHR 和其他进程共享的内存(共享库等),RES 里有一部分是它</span><br><span class="line">S 进程状态,就是前面 ps 讲的 STAT 主状态那个字母</span><br></pre></td></tr></table></figure><p>VIRT/RES/SHR 三列默认单位也是 KiB,和 <code>ps</code> 一致;数字大到一定程度 top 会自动换算并带上单位后缀——etcd 那行 VIRT 显示的 <code>11.3g</code> 就是换算成 GiB 后的结果。摘要区的内存两行更直接,行首就标了单位(<code>KiB Mem</code>、<code>KiB Swap</code>)。</p><p>上面这份输出里有个典型困惑:摘要行 CPU 加起来才用了不到 30%(us 14.3 + sy 12.2),下面 dockerd 一个进程却显示 102.0%——两边根本对不上?因为<strong>两处的分母不一样</strong>:摘要行是把所有核的时间摊平后按整机 100% 计算的;进程列表的 %CPU 是按单核 100% 计算的,多线程进程跑满多个核就会超过 100%(102% 就是占满了一个核再多一点)。所以 8 核机器上某进程 %CPU 400%,对应到摘要行也只贡献 50%。</p><p>看的时候配几个交互键:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">P 按 CPU 占用排序(默认)</span><br><span class="line">M 按内存占用排序</span><br><span class="line">1 展开显示每个 CPU 核的占用——看负载是不是集中在单核上(单线程瓶颈的典型特征)</span><br><span class="line">k 输入 PID 发信号杀进程</span><br><span class="line">q 退出</span><br></pre></td></tr></table></figure><h2 id="free-怎么读:available-才是真正可用的"><a href="#free-怎么读:available-才是真正可用的" class="headerlink" title="free 怎么读:available 才是真正可用的"></a>free 怎么读:available 才是真正可用的</h2><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"> total used free shared buff/cache available</span><br><span class="line">Mem: 15Gi 8.2Gi 1.1Gi 512Mi 6.2Gi 6.7Gi</span><br><span class="line">Swap: 2.0Gi 0Gi 2.0Gi</span><br></pre></td></tr></table></figure><p>最容易误读的就是 free 列:它只是”完全没被碰过的内存”。Linux 会把空闲内存尽量拿去做磁盘缓存(buff/cache),提高文件读写速度——这部分在应用需要时随时可以回收。所以:</p><ul><li><strong>看 available,不看 free</strong>:available = 真正可分配给新程序的内存(含可回收的缓存),free 很小、available 充足是健康状态</li><li><strong>Swap used 持续上涨才是真缺内存</strong>:物理内存实在不够,系统开始把内存页挪到磁盘上,性能会明显劣化——这才是需要处理的信号</li></ul><h2 id="组合实战:机器变慢了怎么定位"><a href="#组合实战:机器变慢了怎么定位" class="headerlink" title="组合实战:机器变慢了怎么定位"></a>组合实战:机器变慢了怎么定位</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">uptime</span></span><br><span class="line"><span class="comment"># 第 1 步:看 load average 和核数的比值,确认是不是真的过载,趋势是升是降</span></span><br><span class="line"></span><br><span class="line">top</span><br><span class="line"><span class="comment"># 第 2 步:过载的话按 P 看谁吃 CPU;CPU 不高就怀疑 IO——</span></span><br><span class="line"><span class="comment"># 看进程列表里有没有一片 D 状态</span></span><br><span class="line"></span><br><span class="line">free -h</span><br><span class="line"><span class="comment"># 第 3 步:看 available 是否枯竭、Swap 是否被大量使用</span></span><br><span class="line"><span class="comment"># 内存不足会连锁引发 CPU 和 IO 症状(频繁回收、swap 读写)</span></span><br><span class="line"></span><br><span class="line">ps aux | awk <span class="string">'$8 ~ /^D/ {print}'</span></span><br><span class="line"><span class="comment"># 第 4 步:列出所有 D 状态的进程(第 8 列是 STAT),确认卡 IO 的是谁</span></span><br></pre></td></tr></table></figure><h2 id="命令参数速查表"><a href="#命令参数速查表" class="headerlink" title="命令参数速查表"></a>命令参数速查表</h2><p>前面重点在”输出怎么读”,命令本身的常用参数也汇总一份。</p><p><strong>ps 常用参数</strong>:</p><table><thead><tr><th>参数</th><th>作用</th></tr></thead><tbody><tr><td><code>a</code></td><td>显示所有用户的进程,不只自己的</td></tr><tr><td><code>u</code></td><td>用户友好格式,带 %CPU/%MEM/VSZ/RSS/STAT 这些列</td></tr><tr><td><code>x</code></td><td>包含没有控制终端的进程(守护进程)</td></tr><tr><td><code>aux</code></td><td>以上三个连用,看全系统进程的标准姿势</td></tr><tr><td><code>-ef</code></td><td>System V 风格的全进程列表,带 PPID(父进程)列</td></tr><tr><td><code>--sort=-%cpu</code></td><td>按 CPU 占用降序,<code>-%mem</code> 则按内存</td></tr><tr><td><code>-p PID</code></td><td>只看指定 PID 的进程</td></tr></tbody></table><p><strong>free 常用参数</strong>:</p><table><thead><tr><th>参数</th><th>作用</th></tr></thead><tbody><tr><td><code>-h</code></td><td>人类可读单位(K/M/G 自动换算)</td></tr><tr><td><code>-m</code> / <code>-g</code></td><td>固定按 MiB / GiB 显示</td></tr><tr><td><code>-s N</code></td><td>每 N 秒刷新一次,持续输出</td></tr><tr><td><code>-t</code></td><td>末尾多一行 Mem+Swap 的合计</td></tr></tbody></table><p><strong>top 启动参数</strong>(不多,常用就这几个):</p><table><thead><tr><th>参数</th><th>作用</th></tr></thead><tbody><tr><td><code>-p PID</code></td><td>只监控指定进程</td></tr><tr><td><code>-d N</code></td><td>刷新间隔改为 N 秒,默认 3 秒</td></tr><tr><td><code>-b -n N</code></td><td>批处理模式输出 N 次后退出,适合重定向到文件留记录</td></tr></tbody></table><h2 id="指标速查表"><a href="#指标速查表" class="headerlink" title="指标速查表"></a>指标速查表</h2><p><strong>ps aux 的指标</strong>:</p><table><thead><tr><th>指标</th><th>怎么读</th></tr></thead><tbody><tr><td>STAT <code>R</code> / <code>S</code></td><td>运行中 / 正常睡眠等事件</td></tr><tr><td>STAT <code>D</code></td><td>卡在 IO,不响应信号(kill -9 也没用),成片出现 = 磁盘/存储瓶颈</td></tr><tr><td>STAT <code>Z</code></td><td>僵尸,处理它的父进程才有用</td></tr><tr><td>STAT <code>T</code></td><td>被暂停(Ctrl-Z 或 SIGSTOP)</td></tr><tr><td>后缀 <code>s</code> / <code>l</code> / <code>+</code></td><td>会话首进程 / 多线程 / 前台进程组</td></tr><tr><td>后缀 <code><</code> / <code>N</code></td><td>高优先级(nice 为负)/ 低优先级(nice 为正)</td></tr><tr><td>后缀 <code>L</code></td><td>内存页锁定不允许换出,数据库进程常见</td></tr><tr><td>VSZ</td><td>虚拟地址空间,单位 KiB,大不代表问题</td></tr><tr><td>RSS</td><td>实际物理内存,单位 KiB,查内存泄漏盯它</td></tr></tbody></table><p><strong>top 的指标</strong>:</p><table><thead><tr><th>指标/操作</th><th>怎么读</th></tr></thead><tbody><tr><td>load average</td><td>除以 <code>nproc</code> 核数看每核负载,1/5/15 分钟对比看趋势</td></tr><tr><td>高 load + 低 CPU</td><td>典型 IO 瓶颈信号,去找 D 状态进程</td></tr><tr><td>%Cpu(s) 的 us / sy</td><td>用户态 / 内核态时间,sy 偏高 = 系统调用太频繁</td></tr><tr><td>%Cpu(s) 的 ni / id</td><td>调过 nice 的进程占比 / 空闲占比</td></tr><tr><td>%Cpu(s) 的 wa</td><td>等 IO 的时间占比,偏高 = IO 瓶颈,与 D 状态互相印证</td></tr><tr><td>%Cpu(s) 的 hi / si</td><td>硬件 / 软件中断,平时接近 0</td></tr><tr><td>%Cpu(s) 的 st</td><td>被宿主机偷走的时间,云服务器上持续偏高说明宿主机超卖</td></tr><tr><td>PR / NI</td><td>内核调度优先级 / nice 值</td></tr><tr><td>VIRT / RES / SHR</td><td>对应 ps 的 VSZ / RSS / 共享内存,默认 KiB,过大自动带 g 后缀</td></tr><tr><td>S</td><td>进程状态,同 ps 的 STAT 主状态</td></tr><tr><td>摘要行 %Cpu(s) 与进程 %CPU 对不上</td><td>分母不同:摘要按整机 100%,进程列按单核 100%</td></tr><tr><td>%CPU > 100%</td><td>多线程占多核的正常现象</td></tr><tr><td>按 <code>P</code> / <code>M</code></td><td>按 CPU / 按内存排序</td></tr><tr><td>按 <code>1</code></td><td>展开每核占用,识别单线程瓶颈</td></tr><tr><td>按 <code>k</code> / <code>q</code></td><td>杀进程 / 退出</td></tr></tbody></table><p><strong>free 的指标</strong>:</p><table><thead><tr><th>指标</th><th>怎么读</th></tr></thead><tbody><tr><td>free 列</td><td>完全没被碰过的内存,很小不代表内存不够</td></tr><tr><td>buff/cache</td><td>系统拿去做磁盘缓存的内存,应用需要时可回收</td></tr><tr><td>available</td><td>真正可分配的内存(含可回收缓存),看这列下结论</td></tr><tr><td>Swap used 上涨</td><td>物理内存真不够的信号,性能开始劣化</td></tr></tbody></table><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p>这三个命令的输出各有一个最值得记的点:<code>ps</code> 看 STAT(尤其警惕成片的 D)、load average 要除以核数再下结论、<code>free</code> 看 available 不看 free。把这几个指标读对,”机器慢”就能拆成 CPU、IO、内存三条线分头去查,而不是盯着满屏数字发懵。</p>]]>
</content>
<id>https://blog.codewater.top/ps-top-free%E8%BE%93%E5%87%BA%E8%A7%A3%E8%AF%BB-STAT%E7%8A%B6%E6%80%81%E7%A0%81%E4%B8%8Eloadaverage/</id>
<link href="https://blog.codewater.top/ps-top-free%E8%BE%93%E5%87%BA%E8%A7%A3%E8%AF%BB-STAT%E7%8A%B6%E6%80%81%E7%A0%81%E4%B8%8Eloadaverage/"/>
<published>2026-07-14T12:43:57.000Z</published>
<summary>ps aux 的 STAT 列什么意思、VSZ/RSS 差在哪、load average 怎么读、free 看哪一列,一篇讲透。</summary>
<title>ps、top、free 输出怎么读:STAT 状态码、load average 与内存指标</title>
<updated>2026-07-16T08:34:49.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="命令行" scheme="https://blog.codewater.top/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
<category term="网络" scheme="https://blog.codewater.top/tags/%E7%BD%91%E7%BB%9C/"/>
<content>
<![CDATA[<p><code>curl</code> 和 <code>wget</code> 都能发 HTTP 请求、都能下载文件,为什么大多数系统里两个都有?因为它们的设计目标不同:<code>curl</code> 是数据传输的瑞士军刀,调接口、看响应头、在脚本里取数据都靠它;<code>wget</code> 是专职下载器,断点续传、批量下载是它的主场。分清分工,用起来就不纠结。</p><span id="more"></span><p>这是 Linux 命令系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>;<a href="/%E7%BD%91%E7%BB%9C%E8%BF%9E%E9%80%9A%E6%80%A7%E8%AF%8A%E6%96%AD-ping-traceroute%E4%B8%8Edig%E5%AE%9E%E6%88%98">网络连通性诊断那篇</a> 的排查流程里,确认网络和 DNS 都正常后验证”服务本身有没有响应”,用的正是 <code>curl</code>。</p><h2 id="curl-的默认行为:发-GET,结果打到屏幕"><a href="#curl-的默认行为:发-GET,结果打到屏幕" class="headerlink" title="curl 的默认行为:发 GET,结果打到屏幕"></a>curl 的默认行为:发 GET,结果打到屏幕</h2><p><code>curl</code> 不带任何参数时发 GET 请求,把响应体原样打印到标准输出——不落盘。这个默认行为正是它适合调试的原因:请求发出去,响应立刻看得见,还能直接接管道处理:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">curl https://api.example.com/health</span><br><span class="line"><span class="comment"># 发 GET 请求,响应体直接打印到终端</span></span><br><span class="line"></span><br><span class="line">curl -s https://api.example.com/users | jq <span class="string">'.[0].name'</span></span><br><span class="line"><span class="comment"># -s:silent,去掉进度条等杂音输出,适合接管道</span></span><br><span class="line"><span class="comment"># 配合 jq 直接从 JSON 响应里取字段</span></span><br></pre></td></tr></table></figure><h2 id="curl-调接口:高频参数"><a href="#curl-调接口:高频参数" class="headerlink" title="curl 调接口:高频参数"></a>curl 调接口:高频参数</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">curl -I https://example.com</span><br><span class="line"><span class="comment"># -I:只请求并显示响应头(实际发的是 HEAD 请求,不下载响应体)</span></span><br><span class="line"><span class="comment"># 看状态码、Content-Type、缓存头,不关心内容时用它最快</span></span><br><span class="line"></span><br><span class="line">curl -v https://api.example.com/user</span><br><span class="line"><span class="comment"># -v:verbose,显示完整的请求和响应过程——包括 DNS 解析、TLS 握手、</span></span><br><span class="line"><span class="comment"># 发出去的请求头、收到的响应头,调试"请求到底发成什么样了"必用</span></span><br><span class="line"></span><br><span class="line">curl -X POST https://api.example.com/login \</span><br><span class="line"> -H <span class="string">"Content-Type: application/json"</span> \</span><br><span class="line"> -d <span class="string">'{"username":"test","password":"123"}'</span></span><br><span class="line"><span class="comment"># -X:指定请求方法;-H:加请求头;-d:请求体数据</span></span><br><span class="line"><span class="comment"># 其实 -d 本身就会把方法切成 POST,这里的 -X POST 可以省略,写出来更直观</span></span><br><span class="line"></span><br><span class="line">curl -L http://example.com</span><br><span class="line"><span class="comment"># -L:跟随 301/302 重定向——curl 默认不跟随,页面搬家了只会给你一个 301 响应</span></span><br><span class="line"></span><br><span class="line">curl --max-time 5 https://api.example.com/health</span><br><span class="line"><span class="comment"># --max-time:整个请求的超时上限(秒),脚本里调接口一定要加,避免卡死</span></span><br><span class="line"></span><br><span class="line">curl -s -o /dev/null -w <span class="string">"%{http_code}\n"</span> https://example.com</span><br><span class="line"><span class="comment"># -w:请求完成后按模板输出元信息,%{http_code} 是状态码</span></span><br><span class="line"><span class="comment"># -o /dev/null 丢弃响应体——组合起来就是"只要状态码",健康检查脚本的标准写法</span></span><br></pre></td></tr></table></figure><h2 id="curl-下载文件"><a href="#curl-下载文件" class="headerlink" title="curl 下载文件"></a>curl 下载文件</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">curl -o app.tar.gz https://example.com/download/app-v2.tar.gz</span><br><span class="line"><span class="comment"># -o:把响应体保存成指定文件名,代替默认的打印到终端</span></span><br><span class="line"></span><br><span class="line">curl -O https://example.com/download/app-v2.tar.gz</span><br><span class="line"><span class="comment"># -O:直接用 URL 里的文件名保存(这里存成 app-v2.tar.gz)</span></span><br></pre></td></tr></table></figure><p>偶尔下载一两个文件,<code>curl -O</code> 够用;但要断点续传、批量下载,就该换 <code>wget</code> 了。</p><h2 id="curl-sSL-sh:装软件时最常见的组合"><a href="#curl-sSL-sh:装软件时最常见的组合" class="headerlink" title="curl -sSL | sh:装软件时最常见的组合"></a>curl -sSL | sh:装软件时最常见的组合</h2><p>很多开源工具的官方安装方式都是这个模式——curl 拉下安装脚本,直接通过管道交给 shell 执行:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">curl -sSL https://example.com/install.sh | sh</span><br><span class="line"><span class="comment"># -s:silent,关掉进度条和统计信息——脚本内容要进管道,进度条会污染输出</span></span><br><span class="line"><span class="comment"># -S:show-error,配合 -s 使用——静默模式下如果请求失败,仍然把错误信息打出来</span></span><br><span class="line"><span class="comment"># 只用 -s 的话连报错都吞掉,下载失败了管道那头收到空内容,莫名其妙</span></span><br><span class="line"><span class="comment"># -L:跟随重定向——GitHub raw、安装脚本短链这类地址几乎都有跳转,不加拿到的是空响应</span></span><br><span class="line"><span class="comment"># | sh:把下载到的脚本内容直接交给 shell 执行,一步完成"下载+安装"</span></span><br></pre></td></tr></table></figure><p><code>-sSL</code> 三个参数几乎总是绑在一起出现,可以当成固定搭配记。另外提醒一句:这个模式等于”把远程内容直接交给 shell 执行”,只对信任的官方源用;对来路不明的脚本,先把 <code>| sh</code> 去掉,把内容下载下来看一眼再决定跑不跑。</p><h2 id="wget-的默认行为:直接存成文件"><a href="#wget-的默认行为:直接存成文件" class="headerlink" title="wget 的默认行为:直接存成文件"></a>wget 的默认行为:直接存成文件</h2><p><code>wget</code> 和 <code>curl</code> 的默认行为正相反:不带参数就把 URL 内容保存成本地文件(文件名取自 URL),从设计上就是给”下载”这个场景准备的。macOS 自带 <code>curl</code> 但不自带 <code>wget</code>,需要 <code>brew install wget</code>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">wget https://example.com/download/app-v2.tar.gz</span><br><span class="line"><span class="comment"># 直接下载保存为 app-v2.tar.gz,带进度条和速度显示</span></span><br><span class="line"></span><br><span class="line">wget -c https://example.com/download/big-file.iso</span><br><span class="line"><span class="comment"># -c:continue,断点续传——下载中断后重跑同一条命令,从断的地方继续</span></span><br><span class="line"><span class="comment"># 下载几个 G 的大文件时,这是 wget 相对 curl 最实用的优势</span></span><br><span class="line"></span><br><span class="line">wget -P /tmp/downloads https://example.com/file.zip</span><br><span class="line"><span class="comment"># -P:指定保存目录,不加就存当前目录</span></span><br><span class="line"></span><br><span class="line">wget -r --no-parent https://example.com/docs/</span><br><span class="line"><span class="comment"># -r:递归下载,把页面里链接的资源一并抓下来</span></span><br><span class="line"><span class="comment"># --no-parent:不往上层目录爬,把范围限制在 /docs/ 之下</span></span><br><span class="line"><span class="comment"># 递归下载对目标网站压力不小,范围一定要限制好,别对着整站开抓</span></span><br></pre></td></tr></table></figure><h2 id="怎么选:一句话分工"><a href="#怎么选:一句话分工" class="headerlink" title="怎么选:一句话分工"></a>怎么选:一句话分工</h2><ul><li>调接口、看响应头、脚本里取数据、健康检查——用 <code>curl</code>,默认输出到 stdout 天生适合这些场景</li><li>下载大文件要断点续传、批量抓取——用 <code>wget</code>,<code>-c</code> 和 <code>-r</code> 是 <code>curl</code> 没有的便利</li><li>只是偶尔下载个小文件,两者都行,<code>curl -O</code> 和 <code>wget</code> 等价,用系统里现成的那个(macOS 现成的是 <code>curl</code>)</li></ul><h2 id="常用参数速查表"><a href="#常用参数速查表" class="headerlink" title="常用参数速查表"></a>常用参数速查表</h2><table><thead><tr><th>命令/参数</th><th>作用</th></tr></thead><tbody><tr><td><code>curl URL</code></td><td>发 GET,响应体打印到终端</td></tr><tr><td><code>curl -I</code></td><td>只看响应头(HEAD 请求)</td></tr><tr><td><code>curl -v</code></td><td>显示完整请求/响应过程,调试用</td></tr><tr><td><code>curl -X 方法</code></td><td>指定请求方法(POST/PUT/DELETE 等)</td></tr><tr><td><code>curl -H "头: 值"</code></td><td>添加请求头,可写多次</td></tr><tr><td><code>curl -d '数据'</code></td><td>发送请求体,同时自动把方法切成 POST</td></tr><tr><td><code>curl -L</code></td><td>跟随重定向,默认不跟随</td></tr><tr><td><code>curl --max-time N</code></td><td>整个请求的超时上限(秒)</td></tr><tr><td><code>curl -o 文件</code></td><td>响应体保存为指定文件名</td></tr><tr><td><code>curl -O</code></td><td>响应体保存为远端 URL 里的文件名</td></tr><tr><td><code>curl -s</code></td><td>静默模式,关掉进度条和统计信息</td></tr><tr><td><code>curl -S</code></td><td>配合 <code>-s</code>:静默但请求失败时仍显示错误</td></tr><tr><td><code>curl -sSL url | sh</code></td><td>安装脚本固定搭配:以上三个参数连用 + 管道执行</td></tr><tr><td><code>curl -w "%{http_code}"</code></td><td>输出状态码等元信息</td></tr><tr><td><code>wget URL</code></td><td>下载保存为文件(默认行为)</td></tr><tr><td><code>wget -c</code></td><td>断点续传</td></tr><tr><td><code>wget -P 目录</code></td><td>指定保存目录</td></tr><tr><td><code>wget -r</code></td><td>递归下载,把页面里链接的资源一并抓下来</td></tr><tr><td><code>wget --no-parent</code></td><td>配合 <code>-r</code>:不向上层目录爬,限制递归范围</td></tr></tbody></table><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p>记住两个默认行为就分清了:<code>curl</code> 默认打到屏幕(所以是调试工具),<code>wget</code> 默认存成文件(所以是下载器)。调接口的高频参数就 <code>-I</code>/<code>-v</code>/<code>-H</code>/<code>-d</code>/<code>-L</code> 这几个,下载大文件认准 <code>wget -c</code>,其余参数用到时查表即可。</p>]]>
</content>
<id>https://blog.codewater.top/curl%E4%B8%8Ewget%E5%AE%9E%E6%88%98-%E8%B0%83%E6%8E%A5%E5%8F%A3%E4%B8%8B%E8%BD%BD%E4%B8%8E%E6%96%AD%E7%82%B9%E7%BB%AD%E4%BC%A0/</id>
<link href="https://blog.codewater.top/curl%E4%B8%8Ewget%E5%AE%9E%E6%88%98-%E8%B0%83%E6%8E%A5%E5%8F%A3%E4%B8%8B%E8%BD%BD%E4%B8%8E%E6%96%AD%E7%82%B9%E7%BB%AD%E4%BC%A0/"/>
<published>2026-07-14T06:16:45.000Z</published>
<summary>curl 调接口、wget 下载:-I/-L/-d 常用参数、断点续传与两个工具怎么分工。</summary>
<title>curl 与 wget 实战:调接口、下载文件与断点续传</title>
<updated>2026-07-18T08:44:36.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="命令行" scheme="https://blog.codewater.top/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
<category term="进程" scheme="https://blog.codewater.top/tags/%E8%BF%9B%E7%A8%8B/"/>
<content>
<![CDATA[<p><code>kill -9</code> 是很多人的肌肉记忆——进程不听话,直接 <code>-9</code> 送走。但 <code>kill</code> 这个命令名其实起得有误导性:它默认发的信号根本不是”杀死”,而是”请求退出”。这两者的区别,决定了进程退出前有没有机会保存数据、释放资源。</p><span id="more"></span><p>这是 Linux 命令系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>;<a href="/%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%8E%92%E6%9F%A5%E6%9C%8D%E5%8A%A1%E9%97%AE%E9%A2%98%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4">服务器排查那篇</a> 讲了用 <code>ps</code>/<code>top</code> 找进程,这篇讲找到之后怎么正确地”管”它。</p><h2 id="信号:内核给进程递的通知条"><a href="#信号:内核给进程递的通知条" class="headerlink" title="信号:内核给进程递的通知条"></a>信号:内核给进程递的通知条</h2><p>信号(signal)是内核发给进程的一种异步通知。大部分信号进程可以选择怎么响应——捕获它执行自定义逻辑、忽略它、或者按默认行为处理;只有极少数信号(比如 SIGKILL)是强制的,进程没有任何选择余地。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">kill</span> -l</span><br><span class="line"><span class="comment"># 列出系统支持的全部信号,有几十个</span></span><br><span class="line"><span class="comment"># 日常真正需要认识的只有三四个:TERM、KILL、HUP</span></span><br></pre></td></tr></table></figure><p><code>kill</code> 命令的作用就是给指定 PID 的进程发信号——发什么信号由参数决定,不指定就发默认的。</p><h2 id="SIGTERM(15):kill-的默认信号,请求优雅退出"><a href="#SIGTERM(15):kill-的默认信号,请求优雅退出" class="headerlink" title="SIGTERM(15):kill 的默认信号,请求优雅退出"></a>SIGTERM(15):kill 的默认信号,请求优雅退出</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">kill</span> 12345</span><br><span class="line"><span class="comment"># 不带任何信号参数,默认发 SIGTERM(15)</span></span><br><span class="line"><span class="comment"># 语义是"请你退出"——进程可以捕获这个信号,先做完清理再退出:</span></span><br><span class="line"><span class="comment"># 保存未写盘的数据、关闭数据库连接、写完最后的日志</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">kill</span> -15 12345</span><br><span class="line"><span class="built_in">kill</span> -TERM 12345</span><br><span class="line"><span class="comment"># 和上面完全等价,显式写出信号编号或名字</span></span><br></pre></td></tr></table></figure><p>绝大多数正经写的服务都注册了 SIGTERM 处理逻辑(Go 里的 <code>signal.Notify</code>、容器里的优雅停机钩子都是响应它),收到后会体面地收尾。这也是 <code>systemctl stop</code>、<code>docker stop</code> 停服务时先发的信号。</p><h2 id="SIGKILL(9):最后手段,不是首选"><a href="#SIGKILL(9):最后手段,不是首选" class="headerlink" title="SIGKILL(9):最后手段,不是首选"></a>SIGKILL(9):最后手段,不是首选</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">kill</span> -9 12345</span><br><span class="line"><span class="comment"># SIGKILL 由内核直接终结进程,进程无法捕获、无法忽略——</span></span><br><span class="line"><span class="comment"># 也就没有任何清理机会:没保存的数据直接丢、锁文件残留、连接不释放</span></span><br></pre></td></tr></table></figure><p><code>-9</code> 的问题不在”杀不死”,恰恰在”杀得太死”:进程的清理逻辑一行都不会执行。数据库进程被 <code>-9</code> 可能留下损坏的数据文件,持锁的进程被 <code>-9</code> 可能让别的进程永远等锁。正确的顺序是:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">kill</span> 12345</span><br><span class="line"><span class="comment"># 第 1 步:先发 SIGTERM,给进程清理的机会</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">sleep</span> 5 && <span class="built_in">kill</span> -0 12345 2>/dev/null && <span class="built_in">kill</span> -9 12345</span><br><span class="line"><span class="comment"># 第 2 步:等几秒,确认还没退出(kill -0 只检测进程存在,不发实际信号),</span></span><br><span class="line"><span class="comment"># 这时候再上 -9 强制终结</span></span><br></pre></td></tr></table></figure><p>把 <code>-9</code> 当默认选项,等于放弃了所有进程善后的机会——它应该是 SIGTERM 无效之后的最后手段。</p><h2 id="SIGHUP(1):重载配置的约定俗成"><a href="#SIGHUP(1):重载配置的约定俗成" class="headerlink" title="SIGHUP(1):重载配置的约定俗成"></a>SIGHUP(1):重载配置的约定俗成</h2><p><code>SIGHUP</code> 历史上表示”终端挂断”(hangup,拨号上网时代的遗产),现在大量守护进程把它重新定义成”重新加载配置文件,但不重启”:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">kill</span> -HUP $(<span class="built_in">cat</span> /var/run/nginx.pid)</span><br><span class="line"><span class="comment"># 给 nginx 主进程发 SIGHUP,nginx 会重读配置并平滑应用,服务不中断</span></span><br><span class="line"><span class="comment"># nginx -s reload 底层做的就是这件事</span></span><br></pre></td></tr></table></figure><p>不是所有程序都遵守这个约定,用之前查一下目标服务的文档;但见到 <code>kill -HUP</code> 时要知道,它的意图大概率是”重载配置”,不是杀进程。</p><h2 id="pkill-pgrep:按名字找和杀,跳过查-PID"><a href="#pkill-pgrep:按名字找和杀,跳过查-PID" class="headerlink" title="pkill / pgrep:按名字找和杀,跳过查 PID"></a>pkill / pgrep:按名字找和杀,跳过查 PID</h2><p><code>pgrep</code> 按进程名查 PID,<code>pkill</code> 按进程名直接发信号——省掉 <code>ps aux | grep xxx</code> 再手动复制 PID 的三步操作:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">pgrep -l nginx</span><br><span class="line"><span class="comment"># -l:连同进程名一起列出所有匹配 "nginx" 的进程 PID</span></span><br><span class="line"></span><br><span class="line">pkill nginx</span><br><span class="line"><span class="comment"># 给所有名字匹配 "nginx" 的进程发 SIGTERM(默认信号和 kill 一样)</span></span><br><span class="line"></span><br><span class="line">pkill -9 -f <span class="string">"python.*worker"</span></span><br><span class="line"><span class="comment"># -f:匹配完整命令行而不只是进程名——适合按启动参数区分同名进程</span></span><br><span class="line"><span class="comment"># 注意 -f 匹配范围大,先用 pgrep -f 确认匹配到的是哪些进程再杀,避免误伤</span></span><br></pre></td></tr></table></figure><p><code>pkill -f</code> 是双刃剑:模式写宽了会把不相干的进程一起杀掉。安全的习惯是先跑一遍同参数的 <code>pgrep -fl</code>,看清楚列表再动手。</p><h2 id="nice-renice:CPU-调度优先级"><a href="#nice-renice:CPU-调度优先级" class="headerlink" title="nice / renice:CPU 调度优先级"></a>nice / renice:CPU 调度优先级</h2><p><code>nice</code> 值决定进程在 CPU 竞争中的优先级,范围 -20 到 19——<strong>数值越低优先级越高</strong>,这个方向和直觉相反:可以理解成”nice 值越高,进程越’客气’,越愿意把 CPU 让给别人”。普通用户只能调高 nice 值(降低优先级),设负值需要 root。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">nice</span> -n 19 tar -czf backup.tar.gz /data</span><br><span class="line"><span class="comment"># -n:指定 nice 值增量,19 是最低优先级</span></span><br><span class="line"><span class="comment"># 跑大型备份、压缩这类不着急的任务时降低优先级,避免拖慢正常服务</span></span><br><span class="line"></span><br><span class="line">renice -n 10 -p 12345</span><br><span class="line"><span class="comment"># renice 调整已经在运行的进程,-p 指定 PID</span></span><br><span class="line"><span class="comment"># 发现某个后台任务吃 CPU 影响了在线服务,不用重启它,直接调低优先级</span></span><br></pre></td></tr></table></figure><h2 id="常用命令速查表"><a href="#常用命令速查表" class="headerlink" title="常用命令速查表"></a>常用命令速查表</h2><table><thead><tr><th>命令/信号</th><th>作用</th></tr></thead><tbody><tr><td><code>kill PID</code></td><td>发默认信号 SIGTERM(15),请求优雅退出</td></tr><tr><td><code>kill -9 PID</code></td><td>SIGKILL,内核强制终结,无清理机会,最后手段</td></tr><tr><td><code>kill -HUP PID</code></td><td>多数守护进程约定为”重载配置不重启”</td></tr><tr><td><code>kill -0 PID</code></td><td>不发实际信号,只检测进程是否存在</td></tr><tr><td><code>kill -l</code></td><td>列出全部信号</td></tr><tr><td><code>pgrep -l 名字</code></td><td>按进程名查 PID</td></tr><tr><td><code>pkill 名字</code></td><td>按进程名发 SIGTERM</td></tr><tr><td><code>pkill -f 模式</code></td><td>按完整命令行匹配,先用 <code>pgrep -fl</code> 确认再杀</td></tr><tr><td><code>nice -n N 命令</code></td><td>以指定优先级启动,-20 最高、19 最低,越低越优先</td></tr><tr><td><code>renice -n N -p PID</code></td><td>调整运行中进程的优先级,负值需要 root</td></tr></tbody></table><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p><code>kill</code> 家族的正确打开方式:默认 SIGTERM 给进程体面退出的机会,几秒后还赖着不走再 <code>-9</code>;重载配置认准 SIGHUP;批量操作用 <code>pkill</code> 但先 <code>pgrep</code> 确认目标。至于 <code>nice</code>,记住”值越低越优先”这个反直觉设定,剩下的就是两条命令的事。</p>]]>
</content>
<id>https://blog.codewater.top/kill%E4%BF%A1%E5%8F%B7%E6%9C%BA%E5%88%B6%E4%B8%8Enice%E4%BC%98%E5%85%88%E7%BA%A7%E5%AE%9E%E6%88%98/</id>
<link href="https://blog.codewater.top/kill%E4%BF%A1%E5%8F%B7%E6%9C%BA%E5%88%B6%E4%B8%8Enice%E4%BC%98%E5%85%88%E7%BA%A7%E5%AE%9E%E6%88%98/"/>
<published>2026-07-14T06:14:50.000Z</published>
<summary>kill 默认发的不是杀死信号:SIGTERM/SIGKILL/SIGHUP 区别、pkill 按名杀进程与 nice 优先级。</summary>
<title>kill 默认不是"杀死":进程信号与 nice 优先级实战</title>
<updated>2026-07-14T06:14:50.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="命令行" scheme="https://blog.codewater.top/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
<category term="磁盘" scheme="https://blog.codewater.top/tags/%E7%A3%81%E7%9B%98/"/>
<content>
<![CDATA[<p>服务器磁盘报警 90%,挨个目录 <code>ls -la</code> 看着都不大,就是找不到空间被谁吃了——这是用错了工具:<code>ls</code> 根本不统计目录树的总占用。查文件和磁盘信息的这组命令,每个回答的问题不一样,对上号才查得动。</p><span id="more"></span><p>这是 Linux 命令系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>。</p><h2 id="ls:列目录内容,先看懂-la-的每一列"><a href="#ls:列目录内容,先看懂-la-的每一列" class="headerlink" title="ls:列目录内容,先看懂 -la 的每一列"></a>ls:列目录内容,先看懂 -la 的每一列</h2><p><code>ls</code> 列出目录内容,默认不显示 <code>.</code> 开头的隐藏文件、不显示详情,只给一排文件名。日常几乎都是配着参数用:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ls</span> -la</span><br><span class="line"><span class="comment"># -l:长格式,每个文件一行,显示权限、大小、时间等详情</span></span><br><span class="line"><span class="comment"># -a:包含隐藏文件(. 开头的)</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">ls</span> -lah</span><br><span class="line"><span class="comment"># -h:把大小从字节数转成人类可读的 K/M/G</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">ls</span> -lt</span><br><span class="line"><span class="comment"># -t:按修改时间排序,最新的在最上面——找"最近改过什么"最快的办法</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">ls</span> -lS</span><br><span class="line"><span class="comment"># -S:按文件大小排序,最大的在最上面</span></span><br></pre></td></tr></table></figure><p><code>ls -l</code> 输出的每一列都有含义,以这行为例:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">-rw-r--r-- 1 baizhe staff 3833 Jul 7 01:20 _config.yml</span><br><span class="line"># 权限 链接数 属主 属组 大小 修改时间 文件名</span><br><span class="line"># 权限列的读法(rwx 和数字模式)见 chmod/chown 那篇文章</span><br></pre></td></tr></table></figure><p>权限列的完整规则在 <a href="/Linux%E6%9D%83%E9%99%90%E4%B8%8E%E5%B1%9E%E4%B8%BB%E7%AE%A1%E7%90%86-chmod-chown%E5%AE%9E%E6%88%98">Linux 权限与属主管理:chmod、chown</a> 里讲过,这里不重复。</p><h2 id="tree:树状展示目录层级"><a href="#tree:树状展示目录层级" class="headerlink" title="tree:树状展示目录层级"></a>tree:树状展示目录层级</h2><p><code>tree</code> 以树状缩进展示目录结构,比 <code>ls -R</code>(递归列出但没有层级视觉)直观得多。它不是系统自带命令,macOS 和很多精简系统默认没有,要先装(<code>brew install tree</code> / <code>apt install tree</code>):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">tree -L 2</span><br><span class="line"><span class="comment"># -L:限制展示深度,这里只展开两层——项目大了不加这个会刷满整个终端</span></span><br><span class="line"></span><br><span class="line">tree -d</span><br><span class="line"><span class="comment"># -d:只显示目录,不列文件,看结构骨架时更清爽</span></span><br></pre></td></tr></table></figure><h2 id="file:不看扩展名,判断文件真实类型"><a href="#file:不看扩展名,判断文件真实类型" class="headerlink" title="file:不看扩展名,判断文件真实类型"></a>file:不看扩展名,判断文件真实类型</h2><p><code>file</code> 读取文件开头的魔数(magic number)来判断真实类型,完全不依赖扩展名——文件没有后缀、或者怀疑后缀是假的(比如伪装成 .jpg 的脚本)时用它:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">file photo.jpg</span><br><span class="line"><span class="comment"># 输出:JPEG image data, ... 1920x1080 —— 连分辨率都能读出来</span></span><br><span class="line"></span><br><span class="line">file /bin/ls</span><br><span class="line"><span class="comment"># 输出:Mach-O 64-bit executable(macOS)或 ELF 64-bit(Linux)</span></span><br><span class="line"><span class="comment"># 二进制可执行文件能识别出格式和 CPU 架构</span></span><br><span class="line"></span><br><span class="line">file mystery_download</span><br><span class="line"><span class="comment"># 没有扩展名的文件也照样识别,判断"这下载下来的到底是什么"时最有用</span></span><br></pre></td></tr></table></figure><h2 id="stat:文件元信息与三个时间戳"><a href="#stat:文件元信息与三个时间戳" class="headerlink" title="stat:文件元信息与三个时间戳"></a>stat:文件元信息与三个时间戳</h2><p><code>stat</code> 显示文件的完整元信息:大小、权限、inode、属主,以及三个容易搞混的时间戳。这三个时间戳的区别值得单独记住:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">stat</span> -x _config.yml <span class="comment"># macOS/BSD 要加 -x 才输出可读格式;Linux/GNU 直接 stat 即可</span></span><br><span class="line"></span><br><span class="line"> Size: 3833 FileType: Regular File</span><br><span class="line"> Mode: (0644/-rw-r--r--) Uid: (501/baizhe) Gid: (20/staff)</span><br><span class="line">Access: Tue Jul 7 02:07:29 2026</span><br><span class="line">Modify: Tue Jul 7 01:20:52 2026</span><br><span class="line">Change: Tue Jul 7 01:20:52 2026</span><br><span class="line"><span class="comment"># Access(atime):文件内容最后一次被读取的时间</span></span><br><span class="line"><span class="comment"># Modify(mtime):文件内容最后一次被修改的时间——ls -l 显示的就是它</span></span><br><span class="line"><span class="comment"># Change(ctime):文件元信息最后一次变更的时间</span></span><br></pre></td></tr></table></figure><p>最容易讲错的是 <code>ctime</code>:它不是”创建时间”(create),是”变更时间”(change)——指的是元信息的变更。给文件 <code>chmod</code> 改个权限,内容一个字节没动,<code>mtime</code> 不变,但 <code>ctime</code> 会更新。排查”文件内容没变但权限什么时候被人改过”,看的就是 <code>ctime</code>。</p><p>注意 <code>stat</code> 是 BSD/GNU 差异比较大的命令:macOS 上直接 <code>stat file</code> 输出一行紧凑格式,要 <code>-x</code> 才有上面这种可读输出;Linux 上正相反,直接 <code>stat file</code> 就是完整格式。跨平台写脚本时别依赖它的输出格式。</p><h2 id="du-和-df:一个查”谁占的”,一个查”还剩多少”"><a href="#du-和-df:一个查”谁占的”,一个查”还剩多少”" class="headerlink" title="du 和 df:一个查”谁占的”,一个查”还剩多少”"></a>du 和 df:一个查”谁占的”,一个查”还剩多少”</h2><p>这两个命令名字长得像,回答的问题完全不同:<code>df</code>(disk free)查文件系统级别的使用率——“这块盘还剩多少”;<code>du</code>(disk usage)统计某个目录树的实际占用——“这个目录一共吃了多少”。磁盘满排查时两个要配合用:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">df</span> -h</span><br><span class="line"><span class="comment"># -h:人类可读单位;输出每个挂载点的总容量、已用、剩余、使用率</span></span><br><span class="line"><span class="comment"># 先用它确认到底是哪个分区满了</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">du</span> -sh /var/log</span><br><span class="line"><span class="comment"># -s:只显示总和,不逐个列出子目录</span></span><br><span class="line"><span class="comment"># -h:人类可读单位</span></span><br><span class="line"><span class="comment"># 统计指定目录树的总占用</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">du</span> -sh /var/* | <span class="built_in">sort</span> -rh | <span class="built_in">head</span> -10</span><br><span class="line"><span class="comment"># 磁盘满排查的经典组合:列出 /var 下每个子目录的占用,</span></span><br><span class="line"><span class="comment"># 按大小逆序排列取前 10,一眼看出谁是大头</span></span><br></pre></td></tr></table></figure><p>一个偶尔会遇到的现象:<code>du</code> 把整个分区加起来的数字比 <code>df</code> 显示的已用空间小很多。常见原因是有文件已经被删除、但还有进程占着它的文件句柄——这种文件 <code>du</code> 扫不到(目录里已经没有它了),<code>df</code> 却依然把它算在已用空间里,重启对应进程空间才会真正释放。</p><h2 id="组合实战:磁盘满了怎么一步步定位"><a href="#组合实战:磁盘满了怎么一步步定位" class="headerlink" title="组合实战:磁盘满了怎么一步步定位"></a>组合实战:磁盘满了怎么一步步定位</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">df</span> -h</span><br><span class="line"><span class="comment"># 第 1 步:确认是哪个分区满了,比如看到 / 用了 92%</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">du</span> -sh /* 2>/dev/null | <span class="built_in">sort</span> -rh | <span class="built_in">head</span> -10</span><br><span class="line"><span class="comment"># 第 2 步:从根目录开始,看哪个顶层目录占用最大</span></span><br><span class="line"><span class="comment"># 2>/dev/null 忽略没权限读的目录的报错</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">du</span> -sh /var/* 2>/dev/null | <span class="built_in">sort</span> -rh | <span class="built_in">head</span> -10</span><br><span class="line"><span class="comment"># 第 3 步:进入最大的目录继续下钻,一层层缩小范围</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">ls</span> -laSh /var/log | <span class="built_in">head</span></span><br><span class="line"><span class="comment"># 第 4 步:定位到具体目录后,按大小排序找出最大的文件</span></span><br></pre></td></tr></table></figure><p>从分区到目录再到具体文件,三四层下钻通常就能定位。日志文件撑满磁盘是最常见的答案,所以很多人直接从 <code>du -sh /var/log</code> 查起。</p><h2 id="常用参数速查表"><a href="#常用参数速查表" class="headerlink" title="常用参数速查表"></a>常用参数速查表</h2><table><thead><tr><th>命令/参数</th><th>作用</th></tr></thead><tbody><tr><td><code>ls -la</code></td><td>长格式列出全部文件(含隐藏)</td></tr><tr><td><code>ls -lt</code> / <code>ls -lS</code></td><td>按修改时间 / 按大小排序</td></tr><tr><td><code>tree -L N</code></td><td>树状展示目录,限制 N 层深度</td></tr><tr><td><code>tree -d</code></td><td>只显示目录</td></tr><tr><td><code>file 文件</code></td><td>按内容魔数判断文件真实类型</td></tr><tr><td><code>stat 文件</code></td><td>查看元信息;macOS 加 <code>-x</code> 才是可读格式</td></tr><tr><td>atime / mtime / ctime</td><td>最后访问 / 内容修改 / 元信息变更时间</td></tr><tr><td><code>df -h</code></td><td>各分区磁盘使用率</td></tr><tr><td><code>du -sh 目录</code></td><td>目录树总占用</td></tr><tr><td><code>du -sh /* | sort -rh</code></td><td>磁盘满排查:按占用从大到小排</td></tr></tbody></table><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p>这组命令按问题分工:文件列表和排序找 <code>ls</code>,目录结构找 <code>tree</code>,文件类型找 <code>file</code>,元信息和时间戳找 <code>stat</code>,磁盘空间 <code>df</code> 定分区、<code>du</code> 定目录。磁盘报警时别再挨个 <code>ls</code> 了——<code>df</code> 加 <code>du</code> 逐层下钻,几条命令就能揪出占空间的元凶。</p>]]>
</content>
<id>https://blog.codewater.top/ls-stat-du-df%E6%96%87%E4%BB%B6%E4%BF%A1%E6%81%AF%E4%B8%8E%E7%A3%81%E7%9B%98%E5%8D%A0%E7%94%A8%E5%AE%9E%E6%88%98/</id>
<link href="https://blog.codewater.top/ls-stat-du-df%E6%96%87%E4%BB%B6%E4%BF%A1%E6%81%AF%E4%B8%8E%E7%A3%81%E7%9B%98%E5%8D%A0%E7%94%A8%E5%AE%9E%E6%88%98/"/>
<published>2026-07-14T06:12:23.000Z</published>
<summary>ls/tree/file/stat 查文件信息,du/df 查磁盘占用:三个时间戳区别与磁盘满排查实战。</summary>
<title>ls、stat、du、df 实战:文件信息与磁盘占用怎么查</title>
<updated>2026-07-14T06:12:23.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="命令行" scheme="https://blog.codewater.top/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
<category term="文本处理" scheme="https://blog.codewater.top/tags/%E6%96%87%E6%9C%AC%E5%A4%84%E7%90%86/"/>
<content>
<![CDATA[<p>想知道一份访问日志里访问最多的 10 个 IP,不用现写一段 <code>awk</code> 脚本——<code>cut</code>、<code>sort</code>、<code>uniq</code>、<code>wc</code> 这几个小工具,管道接起来一行命令就能搞定,比现场想正则、调脚本快得多。</p><span id="more"></span><p>这是 Linux 命令系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>;这几个小工具经常和 <a href="/sed%E4%B8%8Eawk%E5%AE%9E%E6%88%98%E5%AF%B9%E6%AF%94-%E6%96%87%E6%9C%AC%E5%A4%84%E7%90%86%E5%B7%A5%E5%85%B7%E6%80%8E%E4%B9%88%E9%80%89">sed/awk</a> 配合甚至互相替代,简单场景用它们组合,比写一段 <code>awk</code> 脚本更直接。</p><h2 id="cut:按列或按字符截取"><a href="#cut:按列或按字符截取" class="headerlink" title="cut:按列或按字符截取"></a>cut:按列或按字符截取</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">"alice,25,engineer"</span> | <span class="built_in">cut</span> -d<span class="string">','</span> -f1,3</span><br><span class="line"><span class="comment"># -d:指定分隔符,这里是逗号</span></span><br><span class="line"><span class="comment"># -f:选取第几个字段,1,3 表示第 1 和第 3 个,输出 alice,engineer</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">echo</span> <span class="string">"hello world"</span> | <span class="built_in">cut</span> -c1-5</span><br><span class="line"><span class="comment"># -c:按字符位置截取,1-5 表示第 1 到第 5 个字符,输出 hello</span></span><br></pre></td></tr></table></figure><p><code>cut</code> 的局限也很明显:分隔符必须是单个固定字符,遇到连续多个空格分隔的列(比如 <code>ps aux</code> 的输出),<code>cut -d' '</code> 会因为空格数量不固定而截错位置,这种场景交给 <code>awk</code> 的字段拆分更靠谱。</p><h2 id="sort:排序"><a href="#sort:排序" class="headerlink" title="sort:排序"></a>sort:排序</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sort</span> file.txt</span><br><span class="line"><span class="comment"># 默认按字典序(ASCII)排序——数字字符串会按"字符"比较,"10" 排在 "2" 前面,容易踩坑</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">sort</span> -n file.txt</span><br><span class="line"><span class="comment"># -n:按数值大小排序,这时候 "2" 才会排在 "10" 前面</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">sort</span> -r file.txt</span><br><span class="line"><span class="comment"># -r:逆序排序,配合 -n 就是数值从大到小</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">sort</span> -k2,2n file.txt</span><br><span class="line"><span class="comment"># -k2,2:只按第 2 列排序(不写结束列会一直排到行尾,是很多人忽略的细节)</span></span><br><span class="line"><span class="comment"># n 后缀表示这一列按数值比较</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">sort</span> -u file.txt</span><br><span class="line"><span class="comment"># -u:排序的同时去重,等价于 sort | uniq,但只需要一次调用</span></span><br></pre></td></tr></table></figure><p><code>sort</code> 默认是字典序这件事经常被忽略——处理纯数字列却忘了加 <code>-n</code>,排序结果看着”乱”,其实是没按数值比较。</p><h2 id="uniq:去重和统计重复次数"><a href="#uniq:去重和统计重复次数" class="headerlink" title="uniq:去重和统计重复次数"></a>uniq:去重和统计重复次数</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sort</span> file.txt | <span class="built_in">uniq</span></span><br><span class="line"><span class="comment"># 去除相邻的重复行——注意 uniq 只对相邻的重复行生效,输入必须先排序过</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">sort</span> file.txt | <span class="built_in">uniq</span> -c</span><br><span class="line"><span class="comment"># -c:统计每一行出现的次数,输出前面带一个计数</span></span><br></pre></td></tr></table></figure><p><code>uniq</code> 最容易踩的坑:它不会去重整个文件里所有重复的行,只会去除挨在一起的重复行。原始文件里两个相同的行如果隔着别的内容,<code>uniq</code> 根本发现不了——这就是为什么几乎所有 <code>uniq</code> 的用法前面都要先接一个 <code>sort</code>,把相同的内容排到一起,<code>uniq</code> 才能真正生效。</p><h2 id="wc:统计行数、单词数、字节数"><a href="#wc:统计行数、单词数、字节数" class="headerlink" title="wc:统计行数、单词数、字节数"></a>wc:统计行数、单词数、字节数</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">wc</span> -l file.txt</span><br><span class="line"><span class="comment"># -l:统计行数,判断一个文件/输出有多少条记录时最常用</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">wc</span> -w file.txt</span><br><span class="line"><span class="comment"># -w:统计单词数(按空白字符分隔)</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">wc</span> -c file.txt</span><br><span class="line"><span class="comment"># -c:统计字节数</span></span><br></pre></td></tr></table></figure><p><code>grep -c</code> 和 <code>wc -l</code> 经常配合使用:<code>grep "ERROR" file.txt | wc -l</code> 数出日志里报错的行数,比自己写循环计数快得多。</p><h2 id="jq:命令行的-JSON-处理器"><a href="#jq:命令行的-JSON-处理器" class="headerlink" title="jq:命令行的 JSON 处理器"></a>jq:命令行的 JSON 处理器</h2><p>处理 API 响应或者 JSON 格式的日志,<code>jq</code> 是命令行下最常用的工具:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">'{"name":"alice","age":25}'</span> | jq <span class="string">'.name'</span></span><br><span class="line"><span class="comment"># 取单个字段,输出 "alice"(带引号,因为 jq 输出的是 JSON 值)</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">echo</span> <span class="string">'[{"name":"alice"},{"name":"bob"}]'</span> | jq <span class="string">'.[].name'</span></span><br><span class="line"><span class="comment"># .[]:遍历数组里的每一项</span></span><br><span class="line"><span class="comment"># .name:取每一项的 name 字段,依次输出 "alice" 和 "bob"</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">echo</span> <span class="string">'[{"name":"alice","age":25},{"name":"bob","age":17}]'</span> | jq <span class="string">'.[] | select(.age >= 18)'</span></span><br><span class="line"><span class="comment"># select:按条件过滤数组,只保留满足条件的项,这里是只保留 age >= 18 的记录</span></span><br></pre></td></tr></table></figure><p><code>jq</code> 的语法本身是一门小型查询语言,能做的远不止取字段——排序、分组、重新构造 JSON 结构都支持,日常处理 API 响应,这几个基本用法已经能覆盖大部分场景。</p><h2 id="组合实战:统计访问日志里最高频的-IP"><a href="#组合实战:统计访问日志里最高频的-IP" class="headerlink" title="组合实战:统计访问日志里最高频的 IP"></a>组合实战:统计访问日志里最高频的 IP</h2><p>访问日志每行开头是客户端 IP,统计出现次数最多的前 10 个:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'{print $1}'</span> access.log | <span class="built_in">sort</span> | <span class="built_in">uniq</span> -c | <span class="built_in">sort</span> -rn | <span class="built_in">head</span> -10</span><br><span class="line"><span class="comment"># awk '{print $1}':取每行第一列,也就是 IP 地址</span></span><br><span class="line"><span class="comment"># sort:排序,让相同的 IP 排到一起(uniq 生效的前提)</span></span><br><span class="line"><span class="comment"># uniq -c:统计每个 IP 出现的次数</span></span><br><span class="line"><span class="comment"># sort -rn:按次数数值从大到小排序</span></span><br><span class="line"><span class="comment"># head -10:只看前 10 条</span></span><br></pre></td></tr></table></figure><p>五个步骤,每一步只做一件很简单的事,串起来就完成了一个不算简单的统计任务——这正是 Unix 管道哲学的直接体现,也是<a href="/xargs%E7%AE%A1%E9%81%93%E4%B8%8ELinux%E5%91%BD%E4%BB%A4%E8%AE%BE%E8%AE%A1%E5%93%B2%E5%AD%A6">之前 xargs 那篇</a>提到的”小工具、文本接口、自由组合”的具体例子。</p><h2 id="常用参数速查表"><a href="#常用参数速查表" class="headerlink" title="常用参数速查表"></a>常用参数速查表</h2><table><thead><tr><th>命令/参数</th><th>作用</th></tr></thead><tbody><tr><td><code>cut -d -f</code></td><td>按分隔符截取指定字段</td></tr><tr><td><code>cut -c</code></td><td>按字符位置截取</td></tr><tr><td><code>sort -n</code></td><td>按数值大小排序,默认是字典序</td></tr><tr><td><code>sort -r</code></td><td>逆序排序</td></tr><tr><td><code>sort -k N,N</code></td><td>只按第 N 列排序</td></tr><tr><td><code>sort -u</code></td><td>排序同时去重</td></tr><tr><td><code>uniq -c</code></td><td>统计相邻重复行的次数,使用前必须先 sort</td></tr><tr><td><code>wc -l</code></td><td>统计行数</td></tr><tr><td><code>wc -w</code></td><td>统计单词数</td></tr><tr><td><code>wc -c</code></td><td>统计字节数</td></tr><tr><td><code>jq '.field'</code></td><td>取 JSON 字段</td></tr><tr><td><code>jq '.[]'</code></td><td>遍历 JSON 数组</td></tr><tr><td><code>jq 'select(cond)'</code></td><td>按条件过滤</td></tr></tbody></table><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p>这几个工具单独看都很简单,价值在组合——<code>sort</code> 配 <code>uniq</code> 做去重统计,<code>cut</code> 配 <code>wc</code> 做列提取加计数,<code>jq</code> 单独处理 JSON。遇到”想临时统计点什么”的需求,先想想能不能用这几个小工具接一条管道解决,很多时候比打开编辑器写脚本更快。</p>]]>
</content>
<id>https://blog.codewater.top/cut-sort-uniq-wc-jq%E6%96%87%E6%9C%AC%E5%B0%8F%E5%B7%A5%E5%85%B7%E7%BB%84%E5%90%88%E6%8B%B3%E5%AE%9E%E6%88%98/</id>
<link href="https://blog.codewater.top/cut-sort-uniq-wc-jq%E6%96%87%E6%9C%AC%E5%B0%8F%E5%B7%A5%E5%85%B7%E7%BB%84%E5%90%88%E6%8B%B3%E5%AE%9E%E6%88%98/"/>
<published>2026-07-14T02:11:31.000Z</published>
<summary>cut 截取列、sort 排序、uniq 去重计数、wc 统计、jq 处理 JSON,组合替代 awk 脚本。</summary>
<title>cut、sort、uniq、wc、jq 实战:小工具组合怎么顶一个 awk 脚本</title>
<updated>2026-07-14T02:11:31.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="命令行" scheme="https://blog.codewater.top/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
<category term="权限" scheme="https://blog.codewater.top/tags/%E6%9D%83%E9%99%90/"/>
<content>
<![CDATA[<p>需要用另一个身份执行操作时,<code>sudo</code>、<code>su</code>、<code>sudo su -</code> 好几种写法混着搜到的答案都不一样——都能”变成别的用户”,但具体差在哪、该用哪个,很少有人说清楚。</p><span id="more"></span><p>这是 Linux 命令系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>;跟 <a href="/Linux%E6%9D%83%E9%99%90%E4%B8%8E%E5%B1%9E%E4%B8%BB%E7%AE%A1%E7%90%86-chmod-chown%E5%AE%9E%E6%88%98">Linux 权限与属主管理:chmod、chown</a> 是互补关系——那篇讲文件权限本身,这篇讲”谁能以什么身份操作”。</p><h2 id="sudo:临时提权执行单条命令"><a href="#sudo:临时提权执行单条命令" class="headerlink" title="sudo:临时提权执行单条命令"></a>sudo:临时提权执行单条命令</h2><p><code>sudo</code>(superuser do)默认行为是以另一个用户(不指定的话是 root)的身份执行紧跟在后面的这一条命令,执行完就恢复回当前用户——不是切换整个会话。使用前必须在 <code>/etc/sudoers</code> 里被授权(通常是加入 <code>sudo</code> 或 <code>wheel</code> 组),没有授权直接报错拒绝。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart nginx</span><br><span class="line"><span class="comment"># 以 root 身份重启 nginx,执行完这条命令,当前 shell 还是原来的用户身份</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">sudo</span> -u www-data <span class="built_in">whoami</span></span><br><span class="line"><span class="comment"># -u:指定以哪个用户执行,不写默认是 root</span></span><br></pre></td></tr></table></figure><p>需要连续跑好几条 root 命令时,有两种进入”root 状态”的写法,行为不一样:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> -i</span><br><span class="line"><span class="comment"># 完整登录 shell,加载 root 自己的环境变量和 profile,工作目录切到 /root</span></span><br><span class="line"><span class="comment"># -i 是 --login 的简写</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">sudo</span> -s</span><br><span class="line"><span class="comment"># 保留当前用户的环境变量和 shell 配置,只是提权,不会进入 root 的家目录</span></span><br></pre></td></tr></table></figure><p><code>sudo -i</code> 更”干净”——环境完全是 root 自己的,不会被当前用户 shell 里可能存在的异常配置影响;<code>sudo -s</code> 适合只是想临时用 root 权限跑几条命令、又不想离开当前 shell 环境的场景。</p><h2 id="su:切换到另一个用户的完整会话"><a href="#su:切换到另一个用户的完整会话" class="headerlink" title="su:切换到另一个用户的完整会话"></a>su:切换到另一个用户的完整会话</h2><p><code>su</code>(switch user)是切换到另一个用户的整个 shell 会话,不像 <code>sudo</code> 那样执行完一条命令就自动恢复。同样有一个容易被忽略的区别:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">su alice</span><br><span class="line"><span class="comment"># 切换到 alice,但保留当前的环境变量和所在目录($HOME 还是原来的)</span></span><br><span class="line"></span><br><span class="line">su - alice</span><br><span class="line"><span class="comment"># 加了 -(等价 su -l alice):完整登录,环境变量、$HOME、当前工作目录</span></span><br><span class="line"><span class="comment"># 全部换成 alice 自己的,就像重新登录了一次</span></span><br></pre></td></tr></table></figure><p>不加 <code>-</code> 容易踩坑:切换过去之后 <code>$HOME</code>、<code>$PATH</code> 这些变量其实还是原来用户的,跑一些依赖环境变量的脚本可能出现莫名其妙的行为——日常切换身份,<code>su -</code> 才是更安全的默认选择。</p><h2 id="sudo-和-su-该怎么选"><a href="#sudo-和-su-该怎么选" class="headerlink" title="sudo 和 su 该怎么选"></a>sudo 和 su 该怎么选</h2><ul><li>只需要执行单条命令,用 <code>sudo</code>——每次操作都过一遍 sudoers 授权检查,操作有日志可查(记录谁在什么时间执行了什么),更适合团队协作的服务器环境</li><li>需要以另一个身份连续操作一段时间,用 <code>su -</code> 或者 <code>sudo su -</code>(先用 sudo 权限切换到 su,再完整登录)</li><li>现代实践更推荐前者:只在需要的时候临时提权,用完就退出,比长时间挂在一个 root 会话里风险更小</li></ul><h2 id="useradd-usermod:创建和修改用户"><a href="#useradd-usermod:创建和修改用户" class="headerlink" title="useradd / usermod:创建和修改用户"></a>useradd / usermod:创建和修改用户</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">useradd -m -s /bin/bash -G <span class="built_in">sudo</span> alice</span><br><span class="line"><span class="comment"># -m:创建家目录——不加这个参数,且系统的 CREATE_HOME 没打开的话,</span></span><br><span class="line"><span class="comment"># 默认不会创建家目录,这是新建用户后"登录就报错找不到家目录"的常见原因</span></span><br><span class="line"><span class="comment"># -s:指定登录 shell,这里是 bash</span></span><br><span class="line"><span class="comment"># -G:加入附加组,这里是把 alice 加进 sudo 组,让她能使用 sudo 命令</span></span><br></pre></td></tr></table></figure><p>修改已有用户的这些属性,用 <code>usermod</code>,参数含义基本和 <code>useradd</code> 一致:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">usermod -aG docker alice</span><br><span class="line"><span class="comment"># -aG:把 alice 追加(append)进 docker 组</span></span><br><span class="line"><span class="comment"># 注意一定要带 -a,只写 -G 会用新列表整个替换掉原来的附加组,把之前加入的组全部移除</span></span><br><span class="line"></span><br><span class="line">usermod -s /bin/zsh alice</span><br><span class="line"><span class="comment"># 修改 alice 的默认登录 shell</span></span><br></pre></td></tr></table></figure><h2 id="id-whoami:查看当前身份"><a href="#id-whoami:查看当前身份" class="headerlink" title="id / whoami:查看当前身份"></a>id / whoami:查看当前身份</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">id</span></span><br><span class="line"><span class="comment"># 显示当前用户的 UID、GID,以及所属的所有组</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">whoami</span></span><br><span class="line"><span class="comment"># 只输出当前用户名,不显示 UID/GID 和组信息</span></span><br></pre></td></tr></table></figure><p><code>id</code> 信息更全,排查”这个用户到底在哪些组里、有没有权限做某件事”时用它;只是想确认”我现在是谁”,<code>whoami</code> 更快。</p><h2 id="常用命令速查表"><a href="#常用命令速查表" class="headerlink" title="常用命令速查表"></a>常用命令速查表</h2><table><thead><tr><th>命令</th><th>作用</th></tr></thead><tbody><tr><td><code>sudo 命令</code></td><td>以另一个用户(默认 root)身份执行单条命令,执行完恢复原身份</td></tr><tr><td><code>sudo -i</code></td><td>完整登录 shell,加载 root 自己的环境变量,进入 /root</td></tr><tr><td><code>sudo -s</code></td><td>保留当前用户的环境变量,只提权</td></tr><tr><td><code>su 用户名</code></td><td>切换用户但保留当前环境变量和目录</td></tr><tr><td><code>su - 用户名</code></td><td>完整登录,环境变量、$HOME、当前目录全部换成目标用户的</td></tr><tr><td><code>useradd -m</code></td><td>创建用户并生成家目录</td></tr><tr><td><code>useradd -s</code></td><td>指定登录 shell</td></tr><tr><td><code>useradd -G</code></td><td>加入附加组</td></tr><tr><td><code>usermod -aG</code></td><td>追加加入某个组,不会移除已有的附加组</td></tr><tr><td><code>id</code></td><td>查看当前用户的 UID/GID 和所属组</td></tr><tr><td><code>whoami</code></td><td>只查看当前用户名</td></tr></tbody></table><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p><code>sudo</code> 和 <code>su</code> 的核心区别是”临时提权一条命令”还是”切换整个会话”;<code>su</code>/<code>su -</code> 的核心区别是环境变量要不要一起换。记住这两组对比,剩下的 <code>useradd</code>/<code>usermod</code>/<code>id</code>/<code>whoami</code> 都是围绕”这个用户是谁、能做什么”展开的具体操作,日常运维基本够用。</p>]]>
</content>
<id>https://blog.codewater.top/Linux%E7%94%A8%E6%88%B7%E4%B8%8E%E6%9D%83%E9%99%90%E7%AE%A1%E7%90%86-sudo-su%E4%B8%8Euseradd%E5%AE%9E%E6%88%98/</id>
<link href="https://blog.codewater.top/Linux%E7%94%A8%E6%88%B7%E4%B8%8E%E6%9D%83%E9%99%90%E7%AE%A1%E7%90%86-sudo-su%E4%B8%8Euseradd%E5%AE%9E%E6%88%98/"/>
<published>2026-07-14T02:09:44.000Z</published>
<summary>sudo 和 su 区别在哪,useradd 建用户为什么没有家目录:常见权限操作实战。</summary>
<title>Linux 用户与权限管理:sudo、su 与 useradd 实战</title>
<updated>2026-07-14T02:09:44.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="命令行" scheme="https://blog.codewater.top/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
<category term="包管理" scheme="https://blog.codewater.top/tags/%E5%8C%85%E7%AE%A1%E7%90%86/"/>
<content>
<![CDATA[<p>本地 Mac 用 <code>brew</code>,公司服务器是 Ubuntu 用 <code>apt</code>,偶尔还要连一台 CentOS 用 <code>yum</code>——同样是”装个包”,三套命令语法完全不一样,来回切换很容易记混,装个包先愣一下”这里到底该敲哪个”。</p><span id="more"></span><p>这是 Linux 命令系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>。</p><h2 id="三者分别是谁"><a href="#三者分别是谁" class="headerlink" title="三者分别是谁"></a>三者分别是谁</h2><p><code>apt</code>(Advanced Package Tool)是 Debian、Ubuntu 系发行版的包管理器,2016 年 Ubuntu 16.04 引入,把之前 <code>apt-get</code>/<code>apt-cache</code> 最常用的功能整合成一个更友好的命令,默认还带进度条和彩色输出。</p><p><code>yum</code>/<code>dnf</code> 是 RHEL、CentOS、Fedora 系发行版的包管理器。<code>dnf</code> 是 <code>yum</code> 的继任者,Fedora 从第 22 版(2015 年)起默认用 <code>dnf</code>,RHEL 8 / CentOS 8(2019 年)起也切换为默认用 <code>dnf</code>——现在的系统上敲 <code>yum</code>,背后实际调用的通常就是 <code>dnf</code>。</p><p><code>brew</code>(Homebrew)是 macOS(以及部分 Linux 发行版)的包管理器,装在用户目录下,不需要 <code>sudo</code>。</p><h2 id="常用操作对照"><a href="#常用操作对照" class="headerlink" title="常用操作对照"></a>常用操作对照</h2><p>装包、卸载、搜索这几个高频操作,三套命令放在一起看最清楚:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 更新软件源信息(不是升级软件本身,只是刷新"有哪些包、什么版本"这份清单)</span></span><br><span class="line">apt update</span><br><span class="line">dnf check-update</span><br><span class="line">brew update</span><br><span class="line"></span><br><span class="line"><span class="comment"># 安装一个包</span></span><br><span class="line">apt install nginx</span><br><span class="line">dnf install nginx</span><br><span class="line">brew install nginx</span><br><span class="line"></span><br><span class="line"><span class="comment"># 卸载一个包</span></span><br><span class="line">apt remove nginx</span><br><span class="line">dnf remove nginx</span><br><span class="line">brew uninstall nginx</span><br><span class="line"></span><br><span class="line"><span class="comment"># 搜索一个包</span></span><br><span class="line">apt search nginx</span><br><span class="line">dnf search nginx</span><br><span class="line">brew search nginx</span><br><span class="line"></span><br><span class="line"><span class="comment"># 列出已安装的包</span></span><br><span class="line">apt list --installed</span><br><span class="line">dnf list installed</span><br><span class="line">brew list</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看某个包的详细信息</span></span><br><span class="line">apt show nginx</span><br><span class="line">dnf info nginx</span><br><span class="line">brew info nginx</span><br></pre></td></tr></table></figure><p>三者语义基本一一对应,记住”动作”(装/卸/搜/查)而不是死记每个发行版的具体单词,切换起来会顺手很多。</p><h2 id="升级:更新源信息和升级已装软件是两回事"><a href="#升级:更新源信息和升级已装软件是两回事" class="headerlink" title="升级:更新源信息和升级已装软件是两回事"></a>升级:更新源信息和升级已装软件是两回事</h2><p><code>update</code> 类命令容易被误解成”升级软件”,实际上它只刷新本地的软件源索引(有哪些包、哪个版本),真正把已装软件升级到新版本要用另一条命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Debian/Ubuntu:先刷新索引,再升级已装的包</span></span><br><span class="line">apt update && apt upgrade</span><br><span class="line"></span><br><span class="line"><span class="comment"># RHEL/CentOS/Fedora:dnf 的 upgrade 会自动先检查更新</span></span><br><span class="line">dnf upgrade</span><br><span class="line"></span><br><span class="line"><span class="comment"># macOS:brew upgrade 升级所有已装的包(不加包名)</span></span><br><span class="line">brew update && brew upgrade</span><br></pre></td></tr></table></figure><h2 id="仓库和权限模型的本质区别"><a href="#仓库和权限模型的本质区别" class="headerlink" title="仓库和权限模型的本质区别"></a>仓库和权限模型的本质区别</h2><p><code>apt</code> 和 <code>dnf</code> 依赖系统级的仓库配置文件——<code>apt</code> 是 <code>/etc/apt/sources.list</code>(以及 <code>/etc/apt/sources.list.d/</code> 目录下的分文件配置),<code>dnf</code> 是 <code>/etc/yum.repos.d/</code> 下的一堆 <code>.repo</code> 文件。安装软件要写入系统目录(<code>/usr/bin</code>、<code>/etc</code> 等),所以需要 <code>sudo</code> 权限。</p><p><code>brew</code> 走的是完全不同的模型:软件仓库叫 tap,包和依赖全部装在用户目录下——Apple Silicon Mac 上是 <code>/opt/homebrew</code>,Intel Mac 上是 <code>/usr/local</code>(本机实测 <code>brew --prefix</code> 输出 <code>/opt/homebrew</code>)。因为不碰系统目录,<code>brew</code> 全程不需要 <code>sudo</code>,这也是它能在 macOS 这种对系统目录管得很严的系统上顺畅工作的原因。</p><h2 id="yum-还是-dnf:直接用-dnf"><a href="#yum-还是-dnf:直接用-dnf" class="headerlink" title="yum 还是 dnf:直接用 dnf"></a>yum 还是 dnf:直接用 dnf</h2><p>新装的 RHEL 8 / CentOS 8 及更新版本上,<code>yum</code> 命令依然能用,但只是指向 <code>dnf</code> 的兼容别名,背后真正执行的是 <code>dnf</code>。日常直接敲 <code>dnf</code> 就好,看到别人的脚本或者旧文档里写 <code>yum</code>,心里知道它现在等价于 <code>dnf</code> 即可,不用纠结该学哪个。</p><h2 id="三大家族命令速查表"><a href="#三大家族命令速查表" class="headerlink" title="三大家族命令速查表"></a>三大家族命令速查表</h2><table><thead><tr><th>操作</th><th>apt(Debian/Ubuntu)</th><th>dnf(RHEL/CentOS/Fedora)</th><th>brew(macOS)</th></tr></thead><tbody><tr><td>刷新源索引</td><td><code>apt update</code></td><td><code>dnf check-update</code></td><td><code>brew update</code></td></tr><tr><td>安装包</td><td><code>apt install pkg</code></td><td><code>dnf install pkg</code></td><td><code>brew install pkg</code></td></tr><tr><td>卸载包</td><td><code>apt remove pkg</code></td><td><code>dnf remove pkg</code></td><td><code>brew uninstall pkg</code></td></tr><tr><td>搜索包</td><td><code>apt search pkg</code></td><td><code>dnf search pkg</code></td><td><code>brew search pkg</code></td></tr><tr><td>列出已装</td><td><code>apt list --installed</code></td><td><code>dnf list installed</code></td><td><code>brew list</code></td></tr><tr><td>查看包信息</td><td><code>apt show pkg</code></td><td><code>dnf info pkg</code></td><td><code>brew info pkg</code></td></tr><tr><td>升级已装软件</td><td><code>apt upgrade</code></td><td><code>dnf upgrade</code></td><td><code>brew upgrade</code></td></tr><tr><td>是否需要 sudo</td><td>需要</td><td>需要</td><td>不需要</td></tr></tbody></table><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p>三套包管理命令的核心操作就这几个:装、卸、搜、查、升级,语义上是对齐的,差的只是具体单词和权限模型。换发行版或者换到 Mac,不用重新学一遍,对照着上面这份表格套用命令名就行。</p>]]>
</content>
<id>https://blog.codewater.top/apt-yum-dnf-brew%E5%8C%85%E7%AE%A1%E7%90%86%E5%AE%9E%E6%88%98%E5%AF%B9%E6%AF%94/</id>
<link href="https://blog.codewater.top/apt-yum-dnf-brew%E5%8C%85%E7%AE%A1%E7%90%86%E5%AE%9E%E6%88%98%E5%AF%B9%E6%AF%94/"/>
<published>2026-07-14T02:07:32.000Z</published>
<summary>apt、yum/dnf、brew 常用操作怎么对应:安装、卸载、搜索、源配置一次讲清楚。</summary>
<title>apt、yum/dnf、brew 包管理实战:换发行版不用重新查命令</title>
<updated>2026-07-14T02:07:32.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="命令行" scheme="https://blog.codewater.top/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
<category term="网络" scheme="https://blog.codewater.top/tags/%E7%BD%91%E7%BB%9C/"/>
<content>
<![CDATA[<p>“网站打不开”这句话背后可能是完全不同的问题:网络本身断了、中间某一跳路由出故障、域名解析错了,或者目标服务压根没在监听。不分层排查,只会对着一个打不开的页面反复刷新,猜半天猜不出所以然。</p><span id="more"></span><p>这是 Linux 命令系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>;跟之前写的 <a href="/%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%8E%92%E6%9F%A5%E6%9C%8D%E5%8A%A1%E9%97%AE%E9%A2%98%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4">Linux 服务器问题排查</a> 是互补关系——那篇讲的是端口、进程、日志(<code>ss</code>、<code>lsof</code>),这篇补上”网络本身通不通、DNS 解析对不对”这一层。</p><h2 id="ping:先确认最基本的连通性"><a href="#ping:先确认最基本的连通性" class="headerlink" title="ping:先确认最基本的连通性"></a>ping:先确认最基本的连通性</h2><p><code>ping</code> 基于 ICMP 协议,向目标主机发包并等待回应,用来测试连通性和延迟:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">ping -c 3 baidu.com</span><br><span class="line"><span class="comment"># -c 3:只发 3 个探测包就停止,不加这个参数会一直发,要手动 Ctrl-C 停止</span></span><br><span class="line"></span><br><span class="line">64 bytes from 124.237.177.164: icmp_seq=0 ttl=54 time=124.8 ms</span><br><span class="line">64 bytes from 124.237.177.164: icmp_seq=1 ttl=54 time=125.6 ms</span><br><span class="line">64 bytes from 124.237.177.164: icmp_seq=2 ttl=54 time=127.1 ms</span><br><span class="line"><span class="comment"># time:这一个包的往返延迟,数值越大网络越慢</span></span><br><span class="line"><span class="comment"># ttl:包剩余的存活跳数,能大致反映经过了多少层网络设备(不用深究具体数值)</span></span><br></pre></td></tr></table></figure><p>有个陷阱必须提前知道:<strong>ping 不通不等于网络真的不通</strong>。很多服务器和防火墙会主动屏蔽 ICMP 协议(出于安全考虑,避免被用来扫描存活主机),这种情况下 <code>ping</code> 完全无响应,但目标服务本身其实是正常的、能正常访问。遇到 <code>ping</code> 不通,不能直接下结论”网络挂了”,还得结合后面的手段继续排查。</p><h2 id="traceroute:定位问题出在哪一跳"><a href="#traceroute:定位问题出在哪一跳" class="headerlink" title="traceroute:定位问题出在哪一跳"></a>traceroute:定位问题出在哪一跳</h2><p>如果 <code>ping</code> 确实不通,下一步是 <code>traceroute</code>——它能显示数据包从本机到目标主机,中间经过的每一跳路由,帮你定位问题出在链路上的哪一环:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">traceroute baidu.com</span><br><span class="line"></span><br><span class="line"> 1 192.168.1.1 (192.168.1.1) 2.336 ms 2.079 ms 2.250 ms</span><br><span class="line"> 2 10.169.144.154 (10.169.144.154) 31.583 ms</span><br><span class="line"> 10.169.144.158 (10.169.144.158) 13.839 ms</span><br><span class="line"> 10.169.144.186 (10.169.144.186) 7.618 ms</span><br><span class="line"> 3 10.185.174.254 (10.185.174.254) 10.458 ms 6.176 ms 6.550 ms</span><br><span class="line"><span class="comment"># 每一行是一跳,默认每跳发 3 个探测包(所以后面有 3 个时间值)</span></span><br><span class="line"><span class="comment"># 第 2 跳三次探测返回了不同的地址,说明这一跳背后是多条负载均衡的路径,属正常现象</span></span><br><span class="line"><span class="comment"># 某一跳如果显示 * * *,说明这一跳没有响应——可能是这个节点屏蔽了探测包,也可能是真的断在这里</span></span><br></pre></td></tr></table></figure><p><code>traceroute</code> 的原理并不复杂:它给数据包设置一个从 1 开始递增的 TTL(存活跳数),TTL 为 1 的包到第一跳路由器就会被丢弃,路由器同时会回一个”TTL 耗尽”的 ICMP 消息,<code>traceroute</code> 借此拿到第一跳的地址;接着发 TTL 为 2 的包获取第二跳,以此类推,直到包真正到达目标主机为止。</p><p>实战判断很直接:从某一跳开始持续超时或丢包,问题大概率就出在那一跳之后的链路上——如果那一跳是本地路由器,问题在本地网络;如果是运营商中间节点,问题可能不在你这边,得等或者换网络;如果一直跑到最后几跳才出问题,大概率是目标网络那边的事。</p><h2 id="dig-nslookup:DNS-解析诊断"><a href="#dig-nslookup:DNS-解析诊断" class="headerlink" title="dig / nslookup:DNS 解析诊断"></a>dig / nslookup:DNS 解析诊断</h2><p><code>ping</code> 能通、<code>traceroute</code> 也能跑到目标,网站却还是打不开?这时候该查的是域名解析对不对——域名解析错了,访问的可能压根不是你以为的那台服务器。<code>dig</code>(Domain Information Groper)是专门查询 DNS 记录的工具,默认查询目标域名的 A 记录(域名对应的 IPv4 地址):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">dig baidu.com</span><br><span class="line"></span><br><span class="line">;; QUESTION SECTION:</span><br><span class="line">;baidu.com.INA</span><br><span class="line"></span><br><span class="line">;; ANSWER SECTION:</span><br><span class="line">baidu.com.573INA124.237.177.164</span><br><span class="line">baidu.com.573INA111.63.65.103</span><br><span class="line"></span><br><span class="line">;; Query time: 12 msec</span><br><span class="line">;; SERVER: 100.100.100.100#53(100.100.100.100)</span><br><span class="line"><span class="comment"># QUESTION SECTION:查询了什么,这里是查 baidu.com 的 A 记录(域名对应的 IPv4 地址)</span></span><br><span class="line"><span class="comment"># ANSWER SECTION:解析结果,一个域名可能对应多个 IP(负载均衡的常见做法)</span></span><br><span class="line"><span class="comment"># Query time:这次查询耗时多久</span></span><br><span class="line"><span class="comment"># SERVER:这次查询实际使用的是哪个 DNS 服务器——如果解析结果不对,先看看是不是用错了 DNS 服务器</span></span><br></pre></td></tr></table></figure><p><code>nslookup</code> 是另一个查 DNS 的工具,功能和 <code>dig</code> 类似但输出格式更简洁:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">nslookup baidu.com</span><br><span class="line"></span><br><span class="line">Server:100.100.100.100</span><br><span class="line">Address:100.100.100.100#53</span><br><span class="line"></span><br><span class="line">Non-authoritative answer:</span><br><span class="line">Name:baidu.com</span><br><span class="line">Address: 124.237.177.164</span><br><span class="line"><span class="comment"># 一样能看出用的哪个 DNS 服务器(Server)、解析出来的地址(Address)</span></span><br><span class="line"><span class="comment"># 但信息量明显没有 dig 详细——没有 TTL、没有查询耗时</span></span><br></pre></td></tr></table></figure><p>日常排查优先用 <code>dig</code>,信息更全;<code>nslookup</code> 胜在几乎所有系统都自带,个别精简系统没装 <code>dig</code> 时可以顶上用。</p><h2 id="常用参数速查表"><a href="#常用参数速查表" class="headerlink" title="常用参数速查表"></a>常用参数速查表</h2><table><thead><tr><th>命令/参数</th><th>作用</th></tr></thead><tbody><tr><td><code>ping -c N</code></td><td>只发送 N 个探测包后停止,不加会一直发</td></tr><tr><td><code>ping</code> 输出里的 <code>time</code></td><td>单个包的往返延迟</td></tr><tr><td><code>ping</code> 输出里的 <code>ttl</code></td><td>包剩余存活跳数,大致反映经过的网络层数</td></tr><tr><td><code>traceroute -m N</code></td><td>最多探测 N 跳就停止</td></tr><tr><td><code>traceroute -w N</code></td><td>每次探测等待响应的超时时间(秒)</td></tr><tr><td><code>traceroute</code> 输出里的 <code>* * *</code></td><td>该跳没有响应,可能屏蔽了探测包或链路确实断在这里</td></tr><tr><td><code>dig 域名</code></td><td>查询域名的 A 记录,输出 ANSWER SECTION、Query time、SERVER 等信息</td></tr><tr><td><code>dig</code> 输出里的 <code>SERVER</code></td><td>这次查询实际使用的 DNS 服务器</td></tr><tr><td><code>nslookup 域名</code></td><td>查询域名解析结果,输出比 dig 简洁,没有 TTL 和查询耗时</td></tr></tbody></table><h2 id="综合排查流程"><a href="#综合排查流程" class="headerlink" title="综合排查流程"></a>综合排查流程</h2><p>网站/服务连不上,按这个顺序排查效率最高:</p><ol><li><strong><code>ping</code> 测网络层连通性</strong>——通了,跳到第 3 步;不通,先确认目标是不是屏蔽了 ICMP(比如用 <code>curl</code> 试一下能不能正常拿到响应),排除这个可能之后再进第 2 步</li><li><strong><code>traceroute</code> 定位断在哪一跳</strong>——找到持续超时/丢包开始的那一跳,判断问题出在本地网络、中间运营商还是目标网络</li><li><strong><code>dig</code> 确认域名解析对不对</strong>——解析出来的 IP 跟预期不符,问题在 DNS 这一层,检查用的 DNS 服务器或者域名解析配置</li><li><strong>解析也对、网络也通,但服务还是连不上</strong>——大概率是端口没监听或者防火墙拦截,这部分排查见 <a href="/%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%8E%92%E6%9F%A5%E6%9C%8D%E5%8A%A1%E9%97%AE%E9%A2%98%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4">Linux 服务器问题排查</a> 里 <code>ss</code>、<code>lsof</code> 的用法</li></ol><p>这四步覆盖了从”网络本身”到”DNS”再到”目标服务”的完整链路,按顺序排查比凭感觉瞎试快得多。</p><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p><code>ping</code>、<code>traceroute</code>、<code>dig</code> 这三个工具分别对应网络排查的三个不同层次——连通性、路径、解析。记住这个分层,遇到”连不上”这类模糊的问题,就能很快把它拆解成一个个具体的、可以验证的假设,而不是对着报错信息干瞪眼。</p>]]>
</content>
<id>https://blog.codewater.top/%E7%BD%91%E7%BB%9C%E8%BF%9E%E9%80%9A%E6%80%A7%E8%AF%8A%E6%96%AD-ping-traceroute%E4%B8%8Edig%E5%AE%9E%E6%88%98/</id>
<link href="https://blog.codewater.top/%E7%BD%91%E7%BB%9C%E8%BF%9E%E9%80%9A%E6%80%A7%E8%AF%8A%E6%96%AD-ping-traceroute%E4%B8%8Edig%E5%AE%9E%E6%88%98/"/>
<published>2026-07-13T12:40:49.000Z</published>
<summary>网站打不开怎么分层排查:ping 测连通、traceroute 定位断点、dig 查 DNS 解析。</summary>
<title>网络连不上怎么排查:ping、traceroute 与 dig/nslookup 实战</title>
<updated>2026-07-13T12:40:49.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="命令行" scheme="https://blog.codewater.top/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
<category term="sed" scheme="https://blog.codewater.top/tags/sed/"/>
<category term="awk" scheme="https://blog.codewater.top/tags/awk/"/>
<content>
<![CDATA[<p>搜”命令行批量替换文本”,答案里有的用 <code>sed</code>,有的用 <code>awk</code>,看着好像都能解决问题,但不知道自己该学哪个、什么场景该用哪个。两者确实经常一起出现在同一批搜索结果里——但它们解决的其实是不同维度的问题:<code>sed</code> 面向”行”做替换、删除、插入;<code>awk</code> 面向”列”做结构化处理。搞清楚这个区别,选工具就不用再猜了。</p><span id="more"></span><p>这是 Linux 命令系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>。</p><h2 id="sed-基本用法:按行处理文本"><a href="#sed-基本用法:按行处理文本" class="headerlink" title="sed 基本用法:按行处理文本"></a>sed 基本用法:按行处理文本</h2><p><code>sed</code>(stream editor,流编辑器)默认逐行读取输入,对每一行执行指定的编辑命令后输出结果,不提供交互式编辑界面——写脚本自动化批量处理文本,正是它的设计目的。最核心的命令是 <code>s</code>,替换文本:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">sed <span class="string">'s/old/new/'</span> file.txt</span><br><span class="line"><span class="comment"># 把每一行里第一个匹配到的 "old" 替换成 "new",只替换每行的第一处</span></span><br><span class="line"></span><br><span class="line">sed <span class="string">'s/old/new/g'</span> file.txt</span><br><span class="line"><span class="comment"># 加上 g(global),一行里所有匹配到的 "old" 都替换,不只是第一个</span></span><br></pre></td></tr></table></figure><p>默认情况下 <code>sed</code> 不会修改原文件,只是把处理结果打印到标准输出。想直接改文件本身,要加 <code>-i</code>(in-place)——这里有个必须提前知道的跨平台坑:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># macOS / BSD sed</span></span><br><span class="line">sed -i <span class="string">''</span> <span class="string">'s/old/new/'</span> file.txt</span><br><span class="line"><span class="comment"># BSD sed 要求 -i 后面必须紧跟一个备份文件后缀参数,不想要备份就传一个空字符串 ''</span></span><br><span class="line"><span class="comment"># 漏了这个空字符串参数,sed 会把下一个参数(也就是替换脚本本身)当成后缀,直接报错</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># GNU sed(Linux 发行版默认自带的 sed)</span></span><br><span class="line">sed -i <span class="string">'s/old/new/'</span> file.txt</span><br><span class="line"><span class="comment"># GNU sed 的 -i 不需要额外参数,直接原地修改,不生成备份</span></span><br><span class="line"><span class="comment"># 想要备份就不带空格直接写后缀:sed -i.bak 's/old/new/' file.txt,会生成 file.txt.bak</span></span><br></pre></td></tr></table></figure><p>同一条 <code>sed -i</code> 命令,在 macOS 上能跑通,搬到 Linux 服务器上可能报错(或者反过来)——写跨平台脚本时这是最容易被忽略的细节。</p><p>除了替换,<code>sed</code> 还能按行号删除或者操作某个范围:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">sed <span class="string">'2d'</span> file.txt</span><br><span class="line"><span class="comment"># 删除第 2 行(打印到标准输出,加 -i 才会真正改文件)</span></span><br><span class="line"></span><br><span class="line">sed <span class="string">'2,4d'</span> file.txt</span><br><span class="line"><span class="comment"># 删除第 2 到第 4 行(含首尾)</span></span><br><span class="line"></span><br><span class="line">sed -e <span class="string">'s/foo/bar/'</span> -e <span class="string">'s/baz/qux/'</span> file.txt</span><br><span class="line"><span class="comment"># -e:一次执行多个替换表达式,不用为每个替换单独跑一次 sed</span></span><br></pre></td></tr></table></figure><h3 id="sed-常用参数速查表"><a href="#sed-常用参数速查表" class="headerlink" title="sed 常用参数速查表"></a>sed 常用参数速查表</h3><table><thead><tr><th>参数/命令</th><th>作用</th></tr></thead><tbody><tr><td><code>s/old/new/</code></td><td>替换命令,只替换每行第一个匹配</td></tr><tr><td><code>s/old/new/g</code></td><td>加 <code>g</code>,替换每行所有匹配</td></tr><tr><td><code>-i</code></td><td>原地修改文件;macOS/BSD 必须带空字符串参数 <code>-i ''</code>,GNU/Linux 直接 <code>-i</code> 即可</td></tr><tr><td><code>Nd</code></td><td>删除第 N 行,如 <code>2d</code></td></tr><tr><td><code>N,Md</code></td><td>删除第 N 到第 M 行</td></tr><tr><td><code>-e</code></td><td>一次执行多个表达式,不用重复调用 sed</td></tr></tbody></table><h2 id="awk-基本用法:按列处理结构化文本"><a href="#awk-基本用法:按列处理结构化文本" class="headerlink" title="awk 基本用法:按列处理结构化文本"></a>awk 基本用法:按列处理结构化文本</h2><p><code>awk</code> 是一门以”按行扫描、按字段处理”为核心的小型编程语言,处理思路跟 <code>sed</code> 不一样——它默认把每一行按空白字符拆成若干”字段”,用 <code>$1</code>、<code>$2</code> 这些变量取某一列:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">"alice 25 engineer"</span> | awk <span class="string">'{print $1}'</span></span><br><span class="line"><span class="comment"># $1 是第一个字段,输出 alice</span></span><br><span class="line"><span class="comment"># $0 是整行原始内容,$2、$3 依次是第二、第三个字段</span></span><br></pre></td></tr></table></figure><p>几个内置变量,日常最常用:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'{print NF}'</span> file.txt</span><br><span class="line"><span class="comment"># NF:当前这一行有多少个字段(Number of Fields)</span></span><br><span class="line"></span><br><span class="line">awk <span class="string">'{print NR, $0}'</span> file.txt</span><br><span class="line"><span class="comment"># NR:到目前为止已经处理了多少行(Number of Records),是所有输入累计的行号</span></span><br><span class="line"><span class="comment"># 注意 NR 跨文件也是累计的——如果同时处理多个文件,"当前文件内的行号"要用 FNR,不是 NR</span></span><br></pre></td></tr></table></figure><p>按条件过滤是 <code>awk</code> 真正擅长的场景:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'$3 > 100 {print $1}'</span> data.txt</span><br><span class="line"><span class="comment"># 只有第 3 列的值大于 100 的行,才输出这一行的第 1 列</span></span><br><span class="line"><span class="comment"># 条件写在 {} 前面,满足条件的行才会执行后面 {} 里的动作</span></span><br><span class="line"></span><br><span class="line">awk <span class="string">'BEGIN {print "开始处理"} {sum += $2} END {print "总计:", sum}'</span> data.txt</span><br><span class="line"><span class="comment"># BEGIN 块:在处理任何一行之前执行一次,常用来打印表头或者初始化变量</span></span><br><span class="line"><span class="comment"># END 块:在处理完所有行之后执行一次,常用来输出汇总统计结果</span></span><br><span class="line"><span class="comment"># 中间的主体部分对每一行都执行一次,这里是把第 2 列累加进 sum</span></span><br><span class="line"></span><br><span class="line">awk -F<span class="string">','</span> <span class="string">'{print $2}'</span> data.csv</span><br><span class="line"><span class="comment"># -F:指定字段分隔符,默认是空白字符,处理 CSV 这类逗号分隔的文件要显式指定</span></span><br></pre></td></tr></table></figure><h3 id="awk-常用参数与内置变量速查表"><a href="#awk-常用参数与内置变量速查表" class="headerlink" title="awk 常用参数与内置变量速查表"></a>awk 常用参数与内置变量速查表</h3><table><thead><tr><th>参数/变量</th><th>作用</th></tr></thead><tbody><tr><td><code>$0</code></td><td>整行原始内容</td></tr><tr><td><code>$1</code>、<code>$2</code>…</td><td>第 1、第 2……个字段</td></tr><tr><td><code>NF</code></td><td>当前行的字段总数(Number of Fields)</td></tr><tr><td><code>NR</code></td><td>累计处理到第几行,跨文件累计(Number of Records)</td></tr><tr><td><code>FNR</code></td><td>当前文件内的行号,跨文件会重新计数</td></tr><tr><td><code>-F</code></td><td>指定字段分隔符,默认是空白字符</td></tr><tr><td><code>BEGIN {...}</code></td><td>处理任何一行之前执行一次,常用来初始化</td></tr><tr><td><code>END {...}</code></td><td>处理完所有行之后执行一次,常用来输出汇总</td></tr></tbody></table><h2 id="同一个任务,两种工具的差距在哪"><a href="#同一个任务,两种工具的差距在哪" class="headerlink" title="同一个任务,两种工具的差距在哪"></a>同一个任务,两种工具的差距在哪</h2><p>假设有一份访问日志,每行是”路径 响应时间(ms)”,想找出响应时间超过 500ms 的所有路径。用 <code>awk</code> 几乎是原生支持:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">awk <span class="string">'$2 > 500 {print $1}'</span> access.log</span><br><span class="line"><span class="comment"># 一行搞定:按第 2 列的数值判断,满足条件就输出第 1 列</span></span><br></pre></td></tr></table></figure><p>用 <code>sed</code> 做同样的事,会很别扭——<code>sed</code> 的正则匹配本质是”文本模式匹配”,不理解”这一列的值是不是大于 500”这种数值比较,得先把数值范围拆成一堆正则模式去匹配文本形式,写出来又长又脆弱,稍微换一种数字格式就得重新调整正则。这不是 <code>sed</code> 的缺陷,是它压根不是为这类场景设计的——<code>sed</code> 面对的是”文本”,<code>awk</code> 面对的是”字段和数值”。</p><h2 id="该用哪个:判断标准"><a href="#该用哪个:判断标准" class="headerlink" title="该用哪个:判断标准"></a>该用哪个:判断标准</h2><ul><li>只是替换、删除、插入某几行文本,不涉及”按列取值”或者”数值比较”——用 <code>sed</code>,更直接</li><li>需要”取第几列””按某一列的值筛选””跨行做统计汇总”——用 <code>awk</code>,这是它的设计初衷</li><li>两者也能组合使用,比如先用 <code>awk</code> 提取出需要的列,再用 <code>sed</code> 调整输出格式,但没必要为了”只用一个工具”硬凑一个别扭的写法</li></ul><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p><code>sed</code> 和 <code>awk</code> 都是几十年历史的经典 Unix 工具,功能远不止本文列的这些(<code>sed</code> 支持完整的模式空间和保持空间操作,<code>awk</code> 本身就是一门小型编程语言,支持自定义函数、数组),但日常排查和批处理,本文这些用法已经能覆盖大部分场景。选错工具通常不是因为哪个”更强”,而是没看清任务本质是”处理行”还是”处理列”。</p>]]>
</content>
<id>https://blog.codewater.top/sed%E4%B8%8Eawk%E5%AE%9E%E6%88%98%E5%AF%B9%E6%AF%94-%E6%96%87%E6%9C%AC%E5%A4%84%E7%90%86%E5%B7%A5%E5%85%B7%E6%80%8E%E4%B9%88%E9%80%89/</id>
<link href="https://blog.codewater.top/sed%E4%B8%8Eawk%E5%AE%9E%E6%88%98%E5%AF%B9%E6%AF%94-%E6%96%87%E6%9C%AC%E5%A4%84%E7%90%86%E5%B7%A5%E5%85%B7%E6%80%8E%E4%B9%88%E9%80%89/"/>
<published>2026-07-13T12:38:05.000Z</published>
<summary>sed 面向行替换,awk 面向列处理:s 命令、-i 跨平台坑、NF/NR 怎么用与选型判断。</summary>
<title>sed 与 awk 实战对比:什么时候该用哪个</title>
<updated>2026-07-13T12:38:05.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="命令行" scheme="https://blog.codewater.top/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
<category term="rsync" scheme="https://blog.codewater.top/tags/rsync/"/>
<content>
<![CDATA[<p>同步一个几个 G 的目录到服务器,哪怕只改了几个文件,用 <code>scp</code> 或 <code>cp</code> 还是得把整个目录重新传一遍——网络稍微差一点,等待时间就长得离谱。<code>rsync</code> 解决的正是这个问题:只传输真正变化的部分,其余的原样跳过。</p><span id="more"></span><p>这是 Linux 命令系列的一篇,承接 <a href="/Linux%E5%B8%B8%E8%A7%81%E5%91%BD%E4%BB%A4%E4%B8%8E%E5%B7%A5%E5%85%B7%E5%9C%B0%E5%9B%BE">Linux 常见命令与工具地图</a>。</p><h2 id="rsync-为什么”增量”"><a href="#rsync-为什么”增量”" class="headerlink" title="rsync 为什么”增量”"></a>rsync 为什么”增量”</h2><p><code>rsync</code>(remote sync)是一个文件/目录同步工具,既能同步本地磁盘之间的目录,也能通过 SSH 同步到远程主机,默认行为就是只处理源和目标之间有差异的部分,没变化的文件直接跳过。它判断”变了没”,不是简单看文件名或者改动时间——它用的是一套块级别的对比算法(由 Andrew Tridgell 提出,通常直接叫 rsync algorithm):把文件切成一个个数据块,两边同时计算校验和,只有校验和对不上的块才会真正传输,没变的块直接跳过不传。</p><p>这意味着即使是同一个文件,哪怕只改了中间一小段,<code>rsync</code> 也只传那一小段变化的数据,不会把整个文件重传一遍。目录同步同理:目录里没变的文件直接跳过,只有新增、删除、内容变化的文件才会被处理。这是它比 <code>cp</code>/<code>scp</code> 快的根本原因,不是”压缩得更好”这类表面优化。</p><h2 id="最常用的参数组合:-avz"><a href="#最常用的参数组合:-avz" class="headerlink" title="最常用的参数组合:-avz"></a>最常用的参数组合:-avz</h2><p>日常用 <code>rsync</code> 基本都是这个组合起手:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">rsync -avz ./local_dir/ user@host:/remote/path/</span><br><span class="line"><span class="comment"># -a:archive,归档模式,等价于同时打开 -rlptgoD 这几个选项——</span></span><br><span class="line"><span class="comment"># 递归子目录、保留符号链接、保留权限、保留修改时间、保留属组属主、保留设备文件和特殊文件</span></span><br><span class="line"><span class="comment"># 简单说就是"尽量把文件的原始状态完整保留",日常同步基本都要带上</span></span><br><span class="line"><span class="comment"># -v:verbose,显示每个被传输的文件名,方便确认到底同步了什么</span></span><br><span class="line"><span class="comment"># -z:compress,传输过程中压缩数据,跨网络传输时能省带宽;本地磁盘之间同步不需要这个</span></span><br></pre></td></tr></table></figure><p><code>-a</code> 是这几个参数里最关键的一个:不加 <code>-a</code> 的话,<code>rsync</code> 默认不会递归进子目录,也不会保留权限和时间戳这些元信息,同步出来的结果跟预期会差很多。</p><h2 id="P:进度条和断点续传"><a href="#P:进度条和断点续传" class="headerlink" title="-P:进度条和断点续传"></a>-P:进度条和断点续传</h2><p>同步大文件的时候,光有 <code>-avz</code> 还不够——看不到进度,网络一断就得从头再来:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">rsync -avzP bigfile.iso user@host:/remote/path/</span><br><span class="line"><span class="comment"># -P:--partial 和 --progress 两个选项的简写</span></span><br><span class="line"><span class="comment"># --progress:实时显示当前文件的传输进度(速度、百分比、预计剩余时间)</span></span><br><span class="line"><span class="comment"># --partial:传输中断时保留已经传完的部分,下次续传时接着传,不用从头开始</span></span><br></pre></td></tr></table></figure><p><code>-P</code> 对大文件、不稳定的网络环境特别有用——没有 <code>--partial</code> 的话,传到一半中断,之前传的部分会被删掉,下次又得从零开始。</p><h2 id="–delete:让目标和源完全一致"><a href="#–delete:让目标和源完全一致" class="headerlink" title="–delete:让目标和源完全一致"></a>–delete:让目标和源完全一致</h2><p>前面的同步只会”新增/更新”文件,源目录里删掉的文件不会影响目标目录——目标那边的旧文件会一直留着。想让目标和源保持完全一致(源删了,目标也跟着删),要加 <code>--delete</code>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">rsync -avz --delete ./local_dir/ user@host:/remote/path/</span><br><span class="line"><span class="comment"># --delete:目标目录里凡是源目录没有的文件,都会被删除</span></span><br><span class="line"><span class="comment"># 这是个危险参数——用反了方向(源和目标搞反),可能会把不该删的文件删掉</span></span><br></pre></td></tr></table></figure><p><code>--delete</code> 属于”威力大但容易出事”的参数,用之前一定要先确认源和目标的方向没搞反。</p><h2 id="–dry-run:先演练一遍,别直接动手"><a href="#–dry-run:先演练一遍,别直接动手" class="headerlink" title="–dry-run:先演练一遍,别直接动手"></a>–dry-run:先演练一遍,别直接动手</h2><p>尤其是带了 <code>--delete</code> 这种会删除文件的操作,实际执行前应该先跑一遍演练模式,只看会发生什么,不真的执行:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">rsync -avz --delete --dry-run ./local_dir/ user@host:/remote/path/</span><br><span class="line"><span class="comment"># --dry-run(也可以用简写 -n):只打印会做的操作,不实际传输或删除任何文件</span></span><br><span class="line"><span class="comment"># 确认输出的文件列表符合预期之后,去掉 --dry-run 再跑一次真正执行</span></span><br></pre></td></tr></table></figure><p>只要命令里带了 <code>--delete</code>,先 <code>--dry-run</code> 看一眼输出,是能避免”手滑删错文件”最简单也最有效的办法。</p><h2 id="排除规则:跳过不想同步的文件"><a href="#排除规则:跳过不想同步的文件" class="headerlink" title="排除规则:跳过不想同步的文件"></a>排除规则:跳过不想同步的文件</h2><p>项目目录里的 <code>.git</code>、<code>node_modules</code> 这类文件,同步时通常不需要带上:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">rsync -avz --exclude=<span class="string">'.git'</span> --exclude=<span class="string">'node_modules'</span> ./project/ user@host:/remote/project/</span><br><span class="line"><span class="comment"># --exclude:排除匹配指定模式的文件或目录,可以写多次排除多种内容</span></span><br><span class="line"></span><br><span class="line">rsync -avz --exclude-from=<span class="string">'exclude-list.txt'</span> ./project/ user@host:/remote/project/</span><br><span class="line"><span class="comment"># --exclude-from:从文件里批量读取排除规则,规则多的时候比堆一串 --exclude 清晰</span></span><br><span class="line"><span class="comment"># exclude-list.txt 里每行写一条规则,比如:</span></span><br><span class="line"><span class="comment"># .git</span></span><br><span class="line"><span class="comment"># node_modules</span></span><br><span class="line"><span class="comment"># *.log</span></span><br></pre></td></tr></table></figure><h2 id="远程同步和一个经典的坑:源目录带不带末尾斜杠"><a href="#远程同步和一个经典的坑:源目录带不带末尾斜杠" class="headerlink" title="远程同步和一个经典的坑:源目录带不带末尾斜杠"></a>远程同步和一个经典的坑:源目录带不带末尾斜杠</h2><p><code>rsync</code> 同步远程主机走的是 SSH 通道,语法跟 <code>scp</code> 类似但功能强得多:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">rsync -avz ./local_dir/ user@host:/remote/path/</span><br><span class="line"><span class="comment"># 基于 SSH 传输,需要目标主机开着 SSH 服务、当前用户有权限登录</span></span><br><span class="line"><span class="comment"># 跟 scp 语法接近,但增量传输、断点续传这些能力 scp 都没有</span></span><br></pre></td></tr></table></figure><p>源目录路径末尾带不带 <code>/</code>,行为是不一样的——这是 <code>rsync</code> 最容易踩的坑,官方手册专门用例子说明过:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">rsync -av /src/foo /dest</span><br><span class="line"><span class="comment"># 源路径不带末尾斜杠:把 foo 这个目录本身同步过去</span></span><br><span class="line"><span class="comment"># 结果是 /dest/foo/ 里存放 foo 目录的内容——多出了一层 foo 目录</span></span><br><span class="line"></span><br><span class="line">rsync -av /src/foo/ /dest/foo</span><br><span class="line"><span class="comment"># 源路径带末尾斜杠:表示"同步这个目录里的内容",不是目录本身</span></span><br><span class="line"><span class="comment"># 这条命令和上面那条命令,最终产生的结果是一样的——都是 /dest/foo/ 里存放 foo 的内容</span></span><br><span class="line"><span class="comment"># 区别在于目标路径要不要手动多写一层 foo</span></span><br></pre></td></tr></table></figure><p>简单记忆:源路径末尾的 <code>/</code> 决定”要不要在目标里多包一层目录”——同步目录内容用 <code>foo/</code>,同步目录本身(连同目录名)用 <code>foo</code>(不带斜杠),目标那边对应地要不要手动加上这层目录名。搞混这个经常导致同步出来的目录结构和预期差一层,排查起来莫名其妙。</p><h2 id="rsync-常用参数速查表"><a href="#rsync-常用参数速查表" class="headerlink" title="rsync 常用参数速查表"></a>rsync 常用参数速查表</h2><table><thead><tr><th>参数</th><th>作用</th></tr></thead><tbody><tr><td><code>-a</code></td><td>archive,归档模式,等价于 <code>-rlptgoD</code>:递归子目录、保留符号链接/权限/时间戳/属组属主/设备文件</td></tr><tr><td><code>-v</code></td><td>verbose,显示每个被传输的文件名</td></tr><tr><td><code>-z</code></td><td>compress,传输时压缩数据,跨网络同步能省带宽,本地同步不需要</td></tr><tr><td><code>-P</code></td><td><code>--partial --progress</code> 的简写,显示传输进度并支持断点续传</td></tr><tr><td><code>--delete</code></td><td>让目标和源完全一致,删除目标里源没有的文件——危险参数,用前确认方向没搞反</td></tr><tr><td><code>--dry-run</code> / <code>-n</code></td><td>演练模式,只打印会执行的操作,不真的传输或删除任何文件</td></tr><tr><td><code>--exclude</code></td><td>排除匹配指定模式的文件或目录,可以写多次</td></tr><tr><td><code>--exclude-from</code></td><td>从文件批量读取排除规则,规则多时比堆一串 <code>--exclude</code> 清晰</td></tr></tbody></table><h2 id="写到这里"><a href="#写到这里" class="headerlink" title="写到这里"></a>写到这里</h2><p><code>rsync</code> 的核心就是这几件事:增量传输省时间、<code>-P</code> 应付大文件和不稳定网络、<code>--delete</code> 配 <code>--dry-run</code> 保证安全、<code>--exclude</code> 跳过不需要的内容。这几个参数组合起来,日常的目录同步和轻量级备份基本够用——更复杂的定时增量备份场景,通常是在这套参数基础上配合 cron 或者专门的备份工具,但底层同步逻辑还是这些。</p>]]>
</content>
<id>https://blog.codewater.top/rsync%E5%A2%9E%E9%87%8F%E5%90%8C%E6%AD%A5%E5%AE%9E%E6%88%98-%E5%B8%B8%E7%94%A8%E5%8F%82%E6%95%B0%E4%B8%8E%E8%BF%9C%E7%A8%8B%E5%90%8C%E6%AD%A5/</id>
<link href="https://blog.codewater.top/rsync%E5%A2%9E%E9%87%8F%E5%90%8C%E6%AD%A5%E5%AE%9E%E6%88%98-%E5%B8%B8%E7%94%A8%E5%8F%82%E6%95%B0%E4%B8%8E%E8%BF%9C%E7%A8%8B%E5%90%8C%E6%AD%A5/"/>
<published>2026-07-13T12:35:28.000Z</published>
<summary>rsync 为什么比 scp/cp 快:增量同步原理、-avz/-P/--delete 参数与远程同步实战。</summary>
<title>rsync 增量同步实战:常用参数、排除规则与远程同步</title>
<updated>2026-07-13T12:35:28.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="命令行" scheme="https://blog.codewater.top/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
<category term="Emacs" scheme="https://blog.codewater.top/tags/Emacs/"/>
<category term="readline" scheme="https://blog.codewater.top/tags/readline/"/>
<content>
<![CDATA[<p>命令敲到一半打错了个字符,很多人的反应是按住 <code>←</code> 挪半天光标,或者干脆全删了重打。其实 bash、zsh 这些 shell 默认就内置了一整套 Emacs 风格的编辑快捷键,光标跳词、整段删除、剪切粘贴全都有对应按键,不需要任何配置就能用。</p><p>这篇讲清楚这套快捷键怎么用,外加一个进阶技巧:命令太长太复杂的时候,直接把它甩进编辑器里改。</p><span id="more"></span><p>这是终端效率小系列第三篇,承接 <a href="/%E4%B8%8D%E7%86%9F%E6%82%89%E7%9A%84%E5%91%BD%E4%BB%A4%E6%80%8E%E4%B9%88%E5%BF%AB%E9%80%9F%E6%90%9E%E6%87%82-man%E9%A1%B5%E9%9D%A2%E7%BB%93%E6%9E%84%E4%B8%8Eapropos%E9%80%9F%E6%9F%A5">不熟悉的命令怎么快速搞懂</a> 和 <a href="/vim%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C-%E6%A8%A1%E5%BC%8F%E5%88%87%E6%8D%A2%E5%88%B0%E4%BF%9D%E5%AD%98%E9%80%80%E5%87%BA">vim 基本操作</a>——本篇最后一个技巧会直接用到上一篇学的 vim 保存退出操作。</p><h2 id="为什么这套快捷键几乎所有-shell-都能用"><a href="#为什么这套快捷键几乎所有-shell-都能用" class="headerlink" title="为什么这套快捷键几乎所有 shell 都能用"></a>为什么这套快捷键几乎所有 shell 都能用</h2><p>命令行的编辑功能,bash 靠的是 GNU Readline 库,zsh 靠的是自己的 ZLE(Zsh Line Editor)。两者设计上的默认编辑模式都是 Emacs 风格——但这里有个很容易踩的坑:<strong>zsh 有条隐藏规则,只要 <code>$EDITOR</code> 或 <code>$VISUAL</code> 的值里包含字符串 “vi”(<code>vim</code>、<code>nvim</code> 都算在内),zsh 启动时就会自动切换成 vi 键位,而不是 Emacs 键位</strong>。开发者把 <code>$EDITOR</code> 设成 <code>vim</code> 或 <code>nvim</code> 是很常见的习惯,这种情况下 zsh 默认给的其实是 vi 键位,本文这些 Emacs 快捷键很可能大部分不生效或者行为完全对不上。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="variable">$EDITOR</span></span><br><span class="line"><span class="comment"># 先看一眼这个变量的值,只要包含 "vi"(vim、nvim 都算),zsh 大概率已经是 vi 键位了</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">bindkey</span> -e</span><br><span class="line"><span class="comment"># 写进 ~/.zshrc,强制 zsh 使用 Emacs 键位,不再受 $EDITOR 内容影响</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">set</span> -o vi</span><br><span class="line"><span class="comment"># bash 里想切换成 vi 风格的编辑模式(用 hjkl 那一套)用这个</span></span><br><span class="line"><span class="comment"># 但切过去之后,本文这些 Emacs 快捷键大部分失效,两套模式二选一</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">set</span> -o emacs</span><br><span class="line"><span class="comment"># 切回 Emacs 模式,bash 默认就是这个,不受 $EDITOR 影响——这点和 zsh 不一样</span></span><br></pre></td></tr></table></figure><p>不确定自己当前是哪种键位,直接试一下 <code>Ctrl-A</code> 能不能跳到行首:能跳就是 Emacs 键位;没反应或者屏幕上蹦出一个奇怪符号,大概率是 vi 键位——vi 键位下得先按 <code>Esc</code> 进命令模式,<code>0</code> 才是跳到行首。</p><h2 id="行内光标移动:不用方向键也能秒跳"><a href="#行内光标移动:不用方向键也能秒跳" class="headerlink" title="行内光标移动:不用方向键也能秒跳"></a>行内光标移动:不用方向键也能秒跳</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Ctrl-A</span><br><span class="line"><span class="comment"># 跳到行首(A = ahead / 行的开头)</span></span><br><span class="line"></span><br><span class="line">Ctrl-E</span><br><span class="line"><span class="comment"># 跳到行尾(E = end)</span></span><br><span class="line"></span><br><span class="line">Alt-B</span><br><span class="line"><span class="comment"># 向左跳一个单词(B = back)</span></span><br><span class="line"></span><br><span class="line">Alt-F</span><br><span class="line"><span class="comment"># 向右跳一个单词(F = forward)</span></span><br></pre></td></tr></table></figure><p><code>Alt-B</code>/<code>Alt-F</code> 在部分终端里按不出效果——很多终端默认不把 <code>Alt</code> 当 Meta 键处理,需要在终端设置里打开”Use Option as Meta Key”(macOS Terminal.app 的选项,iTerm2 类似)。打不开设置的话,用 <code>Esc</code> 代替 <code>Alt</code> 也一样:先按 <code>Esc</code> 松开,再按 <code>b</code> 或 <code>f</code>,效果等价于 <code>Alt-B</code>/<code>Alt-F</code>。</p><h2 id="删除和剪切:Ctrl-W-U-K-存进-kill-ring,Ctrl-Y-取回"><a href="#删除和剪切:Ctrl-W-U-K-存进-kill-ring,Ctrl-Y-取回" class="headerlink" title="删除和剪切:Ctrl-W/U/K 存进 kill-ring,Ctrl-Y 取回"></a>删除和剪切:Ctrl-W/U/K 存进 kill-ring,Ctrl-Y 取回</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">Ctrl-W</span><br><span class="line"><span class="comment"># 删除光标前面的一个单词(向后删)</span></span><br><span class="line"></span><br><span class="line">Alt-D</span><br><span class="line"><span class="comment"># 删除光标后面的一个单词(向前删)——Ctrl-W 的反方向</span></span><br><span class="line"></span><br><span class="line">Ctrl-K</span><br><span class="line"><span class="comment"># 删除从光标到行尾的所有内容</span></span><br><span class="line"></span><br><span class="line">Ctrl-Y</span><br><span class="line"><span class="comment"># 粘贴——把上一次删除的内容粘贴回来(yank)</span></span><br></pre></td></tr></table></figure><p><code>Ctrl-U</code> 单独拎出来说,因为它在 bash 和 zsh 里行为不一样,是个真实存在的坑:bash 的 <code>Ctrl-U</code>(Readline 的 <code>unix-line-discard</code>)只删从光标到行首的部分,光标后面的内容原样保留;zsh 的 <code>Ctrl-U</code>(不管 vi 键位还是 Emacs 键位)绑定的都是 <code>kill-whole-line</code>,不管光标停在哪,直接清空整行。同一个按键,bash 和 zsh 下的效果是不同的,用 zsh 的人如果照着 bash 的经验去理解 <code>Ctrl-U</code>,会觉得”怎么光标后面的内容也被删了”。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Ctrl-U</span><br><span class="line"><span class="comment"># bash:删除从光标到行首的内容,光标后面的保留</span></span><br><span class="line"><span class="comment"># zsh:不管光标在哪,直接清空整行——这是 zsh 的 kill-whole-line,跟 bash 不是一回事</span></span><br></pre></td></tr></table></figure><p>这几个删除命令背后有个共同点:删掉的内容不是直接消失,而是存进一个叫 kill-ring 的剪贴区,<code>Ctrl-Y</code> 粘贴的就是这里最新的一条。</p><h2 id="清屏和历史命令反向搜索"><a href="#清屏和历史命令反向搜索" class="headerlink" title="清屏和历史命令反向搜索"></a>清屏和历史命令反向搜索</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Ctrl-L</span><br><span class="line"><span class="comment"># 清屏,光标当前这一行会被挪到清空后的屏幕顶部,命令内容不受影响</span></span><br><span class="line"></span><br><span class="line">Ctrl-R</span><br><span class="line"><span class="comment"># 反向搜索历史命令,输入关键词实时匹配之前敲过的命令</span></span><br><span class="line"><span class="comment"># 再按一次 Ctrl-R 跳到更早的一条匹配,按 Enter 执行,按 Esc 只取用不执行</span></span><br></pre></td></tr></table></figure><p><code>Ctrl-R</code> 是历史命令搜索里最常用的一个,比按 <code>↑</code> 翻半天历史记录快得多——尤其是敲过成百上千条命令之后,翻方向键基本翻不到想要的那条。</p><h2 id="进阶技巧:把命令行甩进编辑器里改"><a href="#进阶技巧:把命令行甩进编辑器里改" class="headerlink" title="进阶技巧:把命令行甩进编辑器里改"></a>进阶技巧:把命令行甩进编辑器里改</h2><p><code>Ctrl-A</code>/<code>Ctrl-E</code> 这类单字符编辑对付一行命令够用,但遇到又长又复杂的命令——比如带了好几个换行的多行脚本片段,或者一长串 JSON——在命令行里用方向键和退格键改,效率很低。这种场景该用 <code>Ctrl-X Ctrl-E</code>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Ctrl-X Ctrl-E</span><br><span class="line"><span class="comment"># 把当前命令行的内容甩进编辑器,用完整的编辑器能力改</span></span><br><span class="line"><span class="comment"># 改完保存退出,编辑结果自动回填到命令行——不会直接执行,只是替换命令行内容</span></span><br></pre></td></tr></table></figure><p>用哪个编辑器由 <code>$VISUAL</code>、<code>$EDITOR</code> 这两个环境变量决定,Bash 官方文档写得很明确:依次尝试 <code>$VISUAL</code>、<code>$EDITOR</code>,两个都没设置的话,最后退回 <code>emacs</code>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> EDITOR=vim</span><br><span class="line"><span class="comment"># 把默认编辑器设成 vim,Ctrl-X Ctrl-E 打开的就是 vim</span></span><br><span class="line"><span class="comment"># 没设置这个变量时,bash 会依次尝试 $VISUAL、$EDITOR,都没有就用 emacs</span></span><br></pre></td></tr></table></figure><p>进了编辑器之后,改完用上一篇学的 <code>:wq</code> 或 <code>:x</code> 保存退出(这正是本文系列第二篇的用武之地),命令行会自动变成编辑后的内容,回车才会真正执行。</p><p>有个地方要注意:这个功能在 bash 里是 Readline 自带的,默认已经绑定在 <code>Ctrl-X Ctrl-E</code> 上,装好 bash 就能用;但 zsh 不一样,ZLE 默认编辑模式虽然也是 Emacs,这个”甩进编辑器”的功能却不是默认绑定的,得在 <code>~/.zshrc</code> 里手动开启:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">autoload</span> -Uz edit-command-line</span><br><span class="line"><span class="comment"># 加载 zsh 自带的 edit-command-line 功能模块</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">zle</span> -N edit-command-line</span><br><span class="line"><span class="comment"># 把它注册成一个 ZLE 可用的编辑命令</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">bindkey</span> <span class="string">'^x^e'</span> edit-command-line</span><br><span class="line"><span class="comment"># 绑定到 Ctrl-X Ctrl-E,跟 bash 保持一致的按键习惯</span></span><br></pre></td></tr></table></figure><p>写完存好 <code>.zshrc</code>,<code>source ~/.zshrc</code> 或者重开一个终端窗口生效。</p><h2 id="vim-nvim-跳转到指定行"><a href="#vim-nvim-跳转到指定行" class="headerlink" title="vim/nvim 跳转到指定行"></a>vim/nvim 跳转到指定行</h2><p>编辑长文件时,与其打开后再一步步找到目标行,不如打开的时候就直接带上跳转目标:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">vim +42 access.log</span><br><span class="line"><span class="comment"># 打开文件后直接跳到第 42 行,光标停在那一行</span></span><br><span class="line"></span><br><span class="line">vim +/ERROR access.log</span><br><span class="line"><span class="comment"># 打开文件后跳到第一个匹配 "ERROR" 的位置,等价于打开后手动 /ERROR 回车</span></span><br></pre></td></tr></table></figure><p><code>+42</code> 是 <code>+:42</code> 的简写形式——vim 允许在命令行用 <code>+{Ex命令}</code> 指定打开文件后自动执行的命令,<code>42</code> 本身就是一个合法的行号跳转 Ex 命令;<code>+/pattern</code> 同理,等价于打开后立刻执行一次 <code>/pattern</code> 搜索。</p><p>如果已经在 vim 里面了,跳转用 <code>:42</code> 或者 <code>42G</code>——后者就是<a href="/vim%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C-%E6%A8%A1%E5%BC%8F%E5%88%87%E6%8D%A2%E5%88%B0%E4%BF%9D%E5%AD%98%E9%80%80%E5%87%BA">上一篇 vim 文章</a>里提到的 <code>G</code> 命令,前面加数字表示跳到第几行,和 <code>gg</code>/<code>G</code> 是同一套逻辑的延伸。</p><h2 id="终端效率小系列:写到这里"><a href="#终端效率小系列:写到这里" class="headerlink" title="终端效率小系列:写到这里"></a>终端效率小系列:写到这里</h2><p>三篇写下来,从 <a href="/%E4%B8%8D%E7%86%9F%E6%82%89%E7%9A%84%E5%91%BD%E4%BB%A4%E6%80%8E%E4%B9%88%E5%BF%AB%E9%80%9F%E6%90%9E%E6%87%82-man%E9%A1%B5%E9%9D%A2%E7%BB%93%E6%9E%84%E4%B8%8Eapropos%E9%80%9F%E6%9F%A5">不熟悉的命令怎么快速搞懂</a>,到 <a href="/vim%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C-%E6%A8%A1%E5%BC%8F%E5%88%87%E6%8D%A2%E5%88%B0%E4%BF%9D%E5%AD%98%E9%80%80%E5%87%BA">vim 的基本操作</a>,再到这篇 Emacs 风格的命令行编辑——串起来就是一套完整的终端使用习惯:查文档、编辑文件、编辑命令行,三件事都能用最少的按键完成。剩下的就是练:这些快捷键只有敲进肌肉记忆之后才会真正提速,光看不练效果有限。</p>]]>
</content>
<id>https://blog.codewater.top/%E5%91%BD%E4%BB%A4%E8%A1%8C%E7%BC%96%E8%BE%91%E8%BF%9B%E9%98%B6-Emacs%E5%BF%AB%E6%8D%B7%E9%94%AE%E4%B8%8E%E7%94%A9%E8%BF%9Bvim%E7%BC%96%E8%BE%91/</id>
<link href="https://blog.codewater.top/%E5%91%BD%E4%BB%A4%E8%A1%8C%E7%BC%96%E8%BE%91%E8%BF%9B%E9%98%B6-Emacs%E5%BF%AB%E6%8D%B7%E9%94%AE%E4%B8%8E%E7%94%A9%E8%BF%9Bvim%E7%BC%96%E8%BE%91/"/>
<published>2026-07-12T16:30:02.000Z</published>
<summary>bash/zsh 默认是 Emacs 键位:Ctrl-A/E/W/U/K/Y 怎么用、Ctrl-X Ctrl-E 甩进编辑器改命令,附 vim 跳转指定行技巧。</summary>
<title>命令行编辑进阶:Emacs 快捷键、甩进 vim 编辑命令行与跳转到指定行</title>
<updated>2026-07-12T16:30:02.000Z</updated>
</entry>
<entry>
<author>
<name>Code Water</name>
</author>
<category term="tools" scheme="https://blog.codewater.top/categories/tools/"/>
<category term="CLI" scheme="https://blog.codewater.top/tags/CLI/"/>
<category term="Linux" scheme="https://blog.codewater.top/tags/Linux/"/>
<category term="命令行" scheme="https://blog.codewater.top/tags/%E5%91%BD%E4%BB%A4%E8%A1%8C/"/>
<category term="vim" scheme="https://blog.codewater.top/tags/vim/"/>
<content>
<![CDATA[<p>“怎么退出 vim”是程序员圈子里流传最广的梗之一——一个编辑器要是连怎么关掉都成了笑话,说明它一定有什么地方和你以为的不一样。</p><p>这个”不一样”,就是 vim 最核心的概念:模式。搞懂这一件事,剩下的操作都顺理成章。</p><span id="more"></span><p>这是终端效率小系列第二篇,承接 <a href="/%E4%B8%8D%E7%86%9F%E6%82%89%E7%9A%84%E5%91%BD%E4%BB%A4%E6%80%8E%E4%B9%88%E5%BF%AB%E9%80%9F%E6%90%9E%E6%87%82-man%E9%A1%B5%E9%9D%A2%E7%BB%93%E6%9E%84%E4%B8%8Eapropos%E9%80%9F%E6%9F%A5">不熟悉的命令怎么快速搞懂:man 页面结构、apropos 搜索与 –help 速查</a>。</p><h2 id="vim-最核心的概念:模式"><a href="#vim-最核心的概念:模式" class="headerlink" title="vim 最核心的概念:模式"></a>vim 最核心的概念:模式</h2><p>大部分编辑器打开就能直接打字。vim 不是——它默认打开的是 <strong>Normal 模式</strong>(也叫命令模式),这时候敲键盘不是在输入文字,是在下命令。想打字,得先按 <code>i</code> 切换到 <strong>Insert 模式</strong>;打完字想回去下命令,按 <code>Esc</code> 切回 Normal 模式。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">打开 vim → 默认在 Normal 模式(敲字母是命令)</span><br><span class="line">按 i → 切到 Insert 模式(可以打字了)</span><br><span class="line">按 Esc → 切回 Normal 模式(打字模式关闭,字母重新变成命令)</span><br></pre></td></tr></table></figure><p>新手最常见的翻车场景就是这个:不知道自己在 Normal 模式,对着屏幕一通乱按,结果文件被改得面目全非——因为那些字母全被当成了命令,不是文字。记住一条:<strong>不确定自己在哪个模式,先按 <code>Esc</code>,回到 Normal 模式再说</strong>,这是最安全的复位动作。</p><h2 id="先学怎么退出"><a href="#先学怎么退出" class="headerlink" title="先学怎么退出"></a>先学怎么退出</h2><p>呼应开头的梗,这个必须先讲:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">:q</span><br><span class="line"><span class="comment"># 退出。文件没有改动过才能用,改过没保存会报错拒绝退出</span></span><br><span class="line"></span><br><span class="line">:q!</span><br><span class="line"><span class="comment"># 强制退出,不保存任何改动——改错了想放弃这次编辑,用这个</span></span><br><span class="line"></span><br><span class="line">:w</span><br><span class="line"><span class="comment"># 只保存,不退出,继续编辑</span></span><br><span class="line"></span><br><span class="line">:wq</span><br><span class="line"><span class="comment"># 保存并退出,不管文件有没有改动过,都会写入磁盘</span></span><br><span class="line"></span><br><span class="line">:x</span><br><span class="line"><span class="comment"># 也是保存并退出,但只在文件真的被改动过时才写入磁盘;</span></span><br><span class="line"><span class="comment"># 如果没有改动,:x 不会碰这个文件,文件的修改时间也不会变</span></span><br><span class="line"><span class="comment"># 日常用 :wq 和 :x 效果几乎一样,区别只在"文件没改动时要不要触碰修改时间"这种边缘场景</span></span><br></pre></td></tr></table></figure><p>所有这些命令都以 <code>:</code> 开头,输入前记得先确认自己在 Normal 模式(如果还在 Insert 模式,先按 <code>Esc</code>)。</p><h2 id="Normal-模式下移动光标"><a href="#Normal-模式下移动光标" class="headerlink" title="Normal 模式下移动光标"></a>Normal 模式下移动光标</h2><p>vim 不太依赖方向键,用几个字母键代替(键盘老式布局上这几个键离主键位更近,不用挪手,很多人这么解释这个设计,具体历史已经不可考):</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">h j k l</span><br><span class="line"><span class="comment"># 依次是 左 下 上 右,代替方向键</span></span><br><span class="line"><span class="comment"># 记不住就想 j 像个往下的箭头,k 在它上面对应往上</span></span><br><span class="line"></span><br><span class="line">w</span><br><span class="line"><span class="comment"># 跳到下一个单词的开头</span></span><br><span class="line"></span><br><span class="line">b</span><br><span class="line"><span class="comment"># 跳到上一个单词的开头(back)</span></span><br><span class="line"></span><br><span class="line">e</span><br><span class="line"><span class="comment"># 跳到当前/下一个单词的结尾</span></span><br><span class="line"></span><br><span class="line">0</span><br><span class="line"><span class="comment"># 跳到当前行的行首</span></span><br><span class="line"></span><br><span class="line">$</span><br><span class="line"><span class="comment"># 跳到当前行的行尾</span></span><br><span class="line"></span><br><span class="line">gg</span><br><span class="line"><span class="comment"># 跳到文件开头</span></span><br><span class="line"></span><br><span class="line">G</span><br><span class="line"><span class="comment"># 跳到文件结尾</span></span><br></pre></td></tr></table></figure><p>前面加数字表示重复次数:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">5j</span><br><span class="line"><span class="comment"># 往下移动 5 行,比按 5 次 j 快</span></span><br><span class="line"></span><br><span class="line">3w</span><br><span class="line"><span class="comment"># 往后跳 3 个单词</span></span><br></pre></td></tr></table></figure><h2 id="行内跳转:按字符查找与括号匹配"><a href="#行内跳转:按字符查找与括号匹配" class="headerlink" title="行内跳转:按字符查找与括号匹配"></a>行内跳转:按字符查找与括号匹配</h2><p><code>hjkl</code>/<code>w</code>/<code>b</code>/<code>e</code> 这些是逐字符或逐单词移动,遇到”跳到这一行某个具体符号上”的场景(比如跳到某个逗号、跳到配对的括号),还有更快的办法:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">f,</span><br><span class="line"><span class="comment"># 向右查找光标所在行里下一个 "," 字符,光标直接停在它上面(find)</span></span><br><span class="line"></span><br><span class="line">t,</span><br><span class="line"><span class="comment"># 向右查找下一个 "," 字符,光标停在它前面一位(till,"直到"但不包含)</span></span><br><span class="line"></span><br><span class="line">F,</span><br><span class="line"><span class="comment"># 向左查找上一个 "," 字符,光标停在它上面</span></span><br><span class="line"></span><br><span class="line">T,</span><br><span class="line"><span class="comment"># 向左查找上一个 "," 字符,光标停在它后面一位</span></span><br><span class="line"></span><br><span class="line">;</span><br><span class="line"><span class="comment"># 重复上一次 f/t/F/T 查找,跳到下一个匹配,不用重新敲一次 f 加字符</span></span><br><span class="line"></span><br><span class="line">%</span><br><span class="line"><span class="comment"># 跳到光标所在括号 ( { [ 的配对括号上,来回按 % 可以在一对括号之间横跳</span></span><br></pre></td></tr></table></figure><p>一行很长的代码里要定位到某个符号,<code>f</code> 加上那个字符,比一个字符一个字符挪快得多;改代码时经常要确认括号有没有配对齐,把光标放在括号上按 <code>%</code> 直接跳到另一半,比肉眼数括号可靠。</p><h2 id="Normal-模式下编辑文本"><a href="#Normal-模式下编辑文本" class="headerlink" title="Normal 模式下编辑文本"></a>Normal 模式下编辑文本</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line">i</span><br><span class="line"><span class="comment"># 在光标当前位置之前插入,进入 Insert 模式</span></span><br><span class="line"></span><br><span class="line">a</span><br><span class="line"><span class="comment"># 在光标当前位置之后插入(append),进入 Insert 模式</span></span><br><span class="line"></span><br><span class="line">o</span><br><span class="line"><span class="comment"># 在当前行下面新起一行,进入 Insert 模式</span></span><br><span class="line"></span><br><span class="line">O</span><br><span class="line"><span class="comment"># 在当前行上面新起一行,进入 Insert 模式</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">dd</span></span><br><span class="line"><span class="comment"># 删除光标所在的整行(同时会把这行内容存到剪贴板,等于剪切)</span></span><br><span class="line"></span><br><span class="line">yy</span><br><span class="line"><span class="comment"># 复制光标所在的整行(yank,不删除)</span></span><br><span class="line"></span><br><span class="line">p</span><br><span class="line"><span class="comment"># 粘贴——把上一次 dd 或 yy 存下来的内容,粘贴到光标下面</span></span><br><span class="line"></span><br><span class="line">u</span><br><span class="line"><span class="comment"># 撤销上一步操作</span></span><br><span class="line"></span><br><span class="line">Ctrl-r</span><br><span class="line"><span class="comment"># 重做(撤销"撤销")</span></span><br><span class="line"></span><br><span class="line">x</span><br><span class="line"><span class="comment"># 删除光标所在的这一个字符——精简版的 dd,只删一个字符不是一整行</span></span><br><span class="line"></span><br><span class="line">r字符</span><br><span class="line"><span class="comment"># 把光标所在字符替换成指定字符,替换完自动回到 Normal 模式,不用再按 Esc</span></span><br><span class="line"><span class="comment"># 比如光标停在一个打错的字母上,按 rx 就把它换成 x</span></span><br><span class="line"></span><br><span class="line">cw</span><br><span class="line"><span class="comment"># 修改一个单词——删除光标到当前单词结尾的内容,直接进入 Insert 模式等着输入新内容</span></span><br><span class="line"><span class="comment"># 等于先 dw 再 i,但 cw 是更常用的固定搭配,一步到位</span></span><br><span class="line"></span><br><span class="line">.</span><br><span class="line"><span class="comment"># 重复上一次的修改操作——改完一处后,把光标移到下一个类似的地方按 .,</span></span><br><span class="line"><span class="comment"># 不用重新敲一遍命令,改多处同样的错误时特别省事</span></span><br></pre></td></tr></table></figure><p><code>i</code>/<code>a</code>/<code>o</code>/<code>O</code> 这四个进 Insert 模式的方式,区别就在光标最终停在哪:<code>i</code> 停在原地之前,<code>a</code> 停在原地之后,<code>o</code>/<code>O</code> 分别在下面/上面新开一行。</p><h2 id="Visual-模式:选中一段范围再操作"><a href="#Visual-模式:选中一段范围再操作" class="headerlink" title="Visual 模式:选中一段范围再操作"></a>Visual 模式:选中一段范围再操作</h2><p>前面的编辑命令大多是”整行”或”一个单词”级别,想自己框选一段范围再操作,要用 Visual(可视)模式。进入方式有三种,选中范围的方式不一样:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">v</span><br><span class="line"><span class="comment"># 进入字符可视模式,逐字符选中——像鼠标拖选一样,选中范围不受行/单词边界限制</span></span><br><span class="line"></span><br><span class="line">V</span><br><span class="line"><span class="comment"># 进入行可视模式,整行整行地选,光标上下移动就是按行扩大选区</span></span><br><span class="line"></span><br><span class="line">Ctrl-v</span><br><span class="line"><span class="comment"># 进入块可视模式,选出一个矩形范围,常用来同时给多行的相同列位置加字符</span></span><br></pre></td></tr></table></figure><p>选中范围之后,跟前面学过的编辑命令组合使用,效果作用在整个选区上:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">d</span><br><span class="line"><span class="comment"># 删除选中的范围</span></span><br><span class="line"></span><br><span class="line">y</span><br><span class="line"><span class="comment"># 复制选中的范围(yank)</span></span><br><span class="line"></span><br><span class="line">c</span><br><span class="line"><span class="comment"># 删除选中的范围,直接进入 Insert 模式,改成新内容</span></span><br></pre></td></tr></table></figure><p>举例:用 <code>V</code> 选中三行按 <code>d</code>,一次删掉三行;用 <code>v</code> 选中一段文字(不是整行)按 <code>y</code>,只复制这一小段。选错了想取消,按 <code>Esc</code> 退出 Visual 模式,不会做任何改动。</p><h2 id="查找和替换"><a href="#查找和替换" class="headerlink" title="查找和替换"></a>查找和替换</h2><p>搜索用 <code>/</code>,和<a href="/%E4%B8%8D%E7%86%9F%E6%82%89%E7%9A%84%E5%91%BD%E4%BB%A4%E6%80%8E%E4%B9%88%E5%BF%AB%E9%80%9F%E6%90%9E%E6%87%82-man%E9%A1%B5%E9%9D%A2%E7%BB%93%E6%9E%84%E4%B8%8Eapropos%E9%80%9F%E6%9F%A5">上一篇讲 man 页面查找</a>里 <code>less</code> 的搜索快捷键是同一套逻辑——vim 的分页和搜索机制本来就深受这套习惯影响:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">/关键词</span><br><span class="line"><span class="comment"># 向下搜索,回车确认</span></span><br><span class="line"></span><br><span class="line">n</span><br><span class="line"><span class="comment"># 跳到下一个匹配</span></span><br><span class="line"></span><br><span class="line">N</span><br><span class="line"><span class="comment"># 跳到上一个匹配(大写,方向和 n 相反)</span></span><br></pre></td></tr></table></figure><p>全局替换用 <code>:%s</code>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">:%s/旧文本/新文本/g</span><br><span class="line"><span class="comment"># % 表示作用范围是整个文件(不加 % 只替换当前光标所在行)</span></span><br><span class="line"><span class="comment"># s 是 substitute,替换命令本身</span></span><br><span class="line"><span class="comment"># g 表示每一行里所有匹配都替换,不加 g 的话每行只替换第一个匹配到的</span></span><br></pre></td></tr></table></figure><p><code>%</code> 和 <code>g</code> 这两个符号经常被忽略,结果就是”明明替换了,怎么还有没改的地方”——十有八九是漏了 <code>%</code>(只在当前行生效)或者漏了 <code>g</code>(每行只替换了第一处)。</p><h2 id="常见操作场景"><a href="#常见操作场景" class="headerlink" title="常见操作场景"></a>常见操作场景</h2><p>几个高频的实际场景,用到的都是前面没提过、但一旦知道就回不去的操作。</p><h3 id="跳转后把光标行挪到屏幕舒服的位置:zz-zt-zb"><a href="#跳转后把光标行挪到屏幕舒服的位置:zz-zt-zb" class="headerlink" title="跳转后把光标行挪到屏幕舒服的位置:zz / zt / zb"></a>跳转后把光标行挪到屏幕舒服的位置:zz / zt / zb</h3><p>只滚动屏幕、不动光标,把光标所在行摆到屏幕中间/顶部/底部。</p><p>用 <code>gg</code>/<code>G</code>/搜索跳到目标行后,那一行经常落在屏幕最顶上或最底下,看上下文很费劲。这时候不用挪光标,直接调整视口:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">zz</span><br><span class="line"><span class="comment"># 把光标所在行滚动到屏幕正中间——跳转后最常用的一下</span></span><br><span class="line"></span><br><span class="line">zt</span><br><span class="line"><span class="comment"># 把光标所在行滚动到屏幕顶部(top),适合接下来要往下读代码</span></span><br><span class="line"></span><br><span class="line">zb</span><br><span class="line"><span class="comment"># 把光标所在行滚动到屏幕底部(bottom),适合要往上看前文</span></span><br></pre></td></tr></table></figure><p>这三个命令只滚动屏幕,光标始终停在原来那一行——在编辑器/IDE 里用 vim 插件跳转到函数定义后,按一下 <code>zz</code> 让目标行居中,是最顺手的组合。</p><h3 id="光标不动只滚视野:Ctrl-e-Ctrl-y"><a href="#光标不动只滚视野:Ctrl-e-Ctrl-y" class="headerlink" title="光标不动只滚视野:Ctrl-e / Ctrl-y"></a>光标不动只滚视野:Ctrl-e / Ctrl-y</h3><p>一行一行地滚动屏幕,光标钉在原地不动。</p><p>想瞄一眼当前屏幕外的上下文,但不想让光标离开正在编辑的位置:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Ctrl-e</span><br><span class="line"><span class="comment"># 视口向下滚动一行,光标留在原来的行上不动</span></span><br><span class="line"><span class="comment"># (光标行快被推出屏幕时才会被迫跟着走)</span></span><br><span class="line"></span><br><span class="line">Ctrl-y</span><br><span class="line"><span class="comment"># 视口向上滚动一行,同样不动光标</span></span><br></pre></td></tr></table></figure><h3 id="半页半页地翻:Ctrl-d-Ctrl-u"><a href="#半页半页地翻:Ctrl-d-Ctrl-u" class="headerlink" title="半页半页地翻:Ctrl-d / Ctrl-u"></a>半页半页地翻:Ctrl-d / Ctrl-u</h3><p>上下滚动半页,光标跟着视口走,浏览长文件的主力翻页方式。</p><p>比整页翻(<code>Ctrl-f</code>/<code>Ctrl-b</code>)保留更多上下文,浏览长文件时更常用:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Ctrl-d</span><br><span class="line"><span class="comment"># 向下滚动半页(down),光标跟着视口走,保持在屏幕上的相对位置</span></span><br><span class="line"></span><br><span class="line">Ctrl-u</span><br><span class="line"><span class="comment"># 向上滚动半页(up)</span></span><br></pre></td></tr></table></figure><p>注意 <code>Ctrl-d</code>/<code>Ctrl-u</code> 和上一个场景不一样:它们滚动时光标会跟着视口移动,适合”人在翻文件”;<code>Ctrl-e</code>/<code>Ctrl-y</code> 是”光标钉在原地只挪视野”,适合编辑中途瞄一眼别处。</p><h2 id="终端效率小系列:下一篇"><a href="#终端效率小系列:下一篇" class="headerlink" title="终端效率小系列:下一篇"></a>终端效率小系列:下一篇</h2><p>下一篇讲 Emacs 风格快捷键——包括一个进阶技巧:在 shell 命令行敲了一半的命令,直接甩进 vim/nvim 里编辑,改完存盘退出,编辑结果自动回到命令行。用到的正是这篇学的基础操作。</p>]]>
</content>
<id>https://blog.codewater.top/vim%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C-%E6%A8%A1%E5%BC%8F%E5%88%87%E6%8D%A2%E5%88%B0%E4%BF%9D%E5%AD%98%E9%80%80%E5%87%BA/</id>
<link href="https://blog.codewater.top/vim%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C-%E6%A8%A1%E5%BC%8F%E5%88%87%E6%8D%A2%E5%88%B0%E4%BF%9D%E5%AD%98%E9%80%80%E5%87%BA/"/>
<published>2026-07-12T16:23:35.000Z</published>
<summary>vim 的 Normal/Insert 模式是什么、怎么退出、hjkl 怎么移动、dd/yy/p 怎么编辑、%s 怎么全局替换,一篇学会基础操作。</summary>
<title>vim 基本操作:从模式切换到保存退出</title>
<updated>2026-07-17T06:44:46.000Z</updated>
</entry>
</feed>