服务重启脚本抛出一句bind: address already in use,很多人的第一反应是netstat,第二反应是把整台机器重启一遍。我遇到过太多次类似场面:让他敲一句lsof -i :8080,三秒钟后 PID 就出来了——那是个三天前手工启动的调试进程,父进程早退出了,它自己还挂在后台占着端口。lsof(list open files)在 Linux 上存在了几十年,几乎每台机器都预装,但真正把它当成日常第一工位工具的人并不多。大多数人只在“文件被占用”这一种场景下想起它,用完就忘,完全没意识到它对网络连接、磁盘空间、进程行为同样有很强的解释力。
这篇内容面向的是已经在用 Linux、但还没有把lsof用顺手的人,以及那些隐约知道lsof | grep deleted却不太清楚它为什么能解决磁盘满问题的同学。我会从输出的字段结构讲起,把端口冲突、幽灵文件、性能代价、脚本化这四条主线都拆开,中间穿插我自己踩过的坑和几个容易误判的分支。文中的命令都可以直接复制执行,参数选择背后的理由我也会尽量说清楚,避免你只记住一堆“神秘咒语”。
1. lsof 输出的不是文件清单,而是进程与内核对象的对应关系
很多人第一次看lsof输出会觉得信息量过载:九列字段、几十上百行、名字里还夹杂着TCP、IPv6、DIR、CHR这些看不懂的类型。理解它的关键是想清楚它到底在查什么——它不是“列出磁盘上的文件”,而是遍历/proc下每个进程的fd目录,把“哪个进程拿到了哪个内核对象的句柄”汇总成一张表。这个定位一旦明确,后面所有参数的意义就都顺了。
1.1 从一次“文件正被占用”的场景说起
假设你在做一次版本发布,想把旧目录整个删掉重铺,rm -rf /data/app/old没报错,但目录还在,或者删完之后du显示的空间没降下来。这时候lsof的价值就体现出来了——它能告诉你“还有谁攥着这个文件不放”。
# 谁打开了这个具体文件 lsof /data/app/old/config.yaml # 谁在这个目录下打开了任意文件(注意:这是逐层扫描,慢) lsof +D /data/app/old这里有个很重要的区别要记牢:lsof /path/to/file只匹配这一个路径,速度很快,因为它只需要在句柄表里比对字符串;而lsof +D /path会把目录递归展开,对每一个文件再逐一比对,开销随目录规模线性上升。我在一个包含十几万小文件的日志目录上跑过+D,等了将近两分钟才返回。真想按目录查,更稳的做法是先定位到具体文件,或者用lsof -c 进程名缩小候选范围再过滤。
另外要提醒一点:删除操作失败、提示Device or resource busy,通常意味着该目录本身被当成了某个进程的工作目录(cwd),而不是里面某个文件被打开。这时直接查目录就能看到:
lsof +D /data/app/old 2>/dev/null | grep cwd看到cwd对应的进程,你就知道是哪个服务把工作目录设在了这里。这种情况在手工cd进去启动的程序、或者 systemd 单元里WorkingDirectory=配错的场景下特别常见。
1.2 九个字段逐列拆开看,每条记录从哪来
一条典型的lsof记录长这样:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME nginx 1204 root cwd DIR 253,0 4096 128 / nginx 1204 root 4u IPv4 29831 0t0 TCP *:80 (LISTEN) nginx 1204 root 8w REG 253,0 1048576 394821 /var/log/nginx/access.log逐列说明如下,这张表我建议你贴在工位旁边:
| 列名 | 含义 | 容易误解的地方 |
|---|---|---|
| COMMAND | 进程名 | 默认只显示前 9 个字符,长名字会被截断,需要+c 0显示全名 |
| PID | 进程号 | 内核线程通常没有句柄,所以基本看不到 |
| USER | 进程属主 | 容器里显示的可能是宿主机映射后的 UID,不一定是用户名 |
| FD | 文件描述符 | 信息密度最高的一列,后面单独说 |
| TYPE | 内核对象类型 | REG普通文件、DIR目录、IPv4/IPv6套接字、CHR字符设备、PIPE管道、unix域套接字 |
| DEVICE | 设备号 | 主设备号加次设备号,排查挂载点归属时会用到 |
| SIZE/OFF | 大小或偏移 | 普通文件是大小,网络套接字是偏移量,两种含义混在同一列 |
| NODE | inode 或协议号 | 普通文件是 inode,网络文件这里是协议号(TCP 就是 6) |
| NAME | 名称 | 普通文件是路径,套接字是地址加状态,管道是pipe加编号 |
TYPE这一列值得单独强调。当你看到IPv4后面跟着ESTABLISHED,说明这是一条活跃连接而不是监听;看到unix说明是本地域套接字,比如数据库的 socket 文件、systemd 的 journal 通道。很多“端口到底有没有被监听”的争论,就是因为没看清(LISTEN)这个后缀。
还有一个细节:如果进程的某个句柄指向一个已经被删除的文件,NAME末尾会带(deleted)标记。这个标记是第 3 章的主角,先记住它。
1.3 FD 列才是信息量最大的地方
FD列看起来是数字,实际上混合了固定关键字和描述符编号两类内容,理解它才能判断进程当前的运行状态:
cwd:当前工作目录txt:可执行文件本身(text)mem:内存映射文件,动态库基本都在这里mmap:映射的设备或共享内存rtd:根目录,容器场景下经常能看到- 纯数字:真正的文件描述符
- 数字后面的
r、w、u:读、写、读写模式 - 带
DEL标记的固定项:说明对象已被删除
举个实战中的用法。某次排查一个服务“看起来在跑但不响应请求”,我用lsof -p <PID>看它的句柄表,发现0u、1u都指向/dev/null,说明标准输入输出被重定向掉了,日志自然哪儿都找不到。这种在守护进程化之后丢失输出的情况非常普遍,知道了就一句话能定位。
再比如判断进程的当前工作目录和实际加载的配置文件:
lsof -p 1204 | grep -E 'cwd|txt|\.conf'这比ps -ef | grep有用得多,因为ps只告诉你启动命令,而启动命令里的相对路径经过了cd、软链接、配置重载之后,往往和实际加载的文件不是同一个。
2. 端口冲突排查:从报错到锁定进程的完整链路
端口被占用是lsof最高频的使用场景,但绝大多数人只停留在“查一下 PID,然后 kill”的层面。真正省时间的做法是把整条链路想清楚:报错说明什么、查出来的结果为什么可能为空、找到进程之后该做哪个分支决策。
2.1 报错出现之后,第一个动作不该是重启
Address already in use这个错误来自内核的bind系统调用,意思是“你要绑定的这个地址加端口组合,已经被另一个套接字占用了”。它有三个常见变体,对应的处理方式完全不同:
第一种是端口真的被别的进程占着,这是最容易处理的,查出 PID 再做决策即可。
第二种是同一个程序的前一个实例没退干净。这种情况在多进程管理方式切换时特别常见,比如原来用手工nohup启动,后来改成了系统服务管理,切换期间两份进程同时存在。这时候杀进程要确认父进程是谁,否则可能出现“杀了又自动拉起”的循环。
第三种是端口处于TIME_WAIT状态。这时候没有进程在监听,但内核还保留着这条连接的残留状态。lsof对这种状态的显示和监听不一样,NAME列末尾是(TIME_WAIT)而不是(LISTEN)。这类问题靠重启挡不住,只能等或者调整内核参数,但我不建议一上来就改内核参数,绝大多数情况下等一分钟就好了,改参数往往会带来别的问题。
.NET或者 Java 应用还有个经典情况:配置里写的是0.0.0.0:8080,但实际绑定失败,原因是 IPv6 的双栈监听已经占了同一个端口。这时候用lsof -i :8080会同时看到 IPv6 和 IPv4 两条记录,得仔细比对。
2.2-i的组合写法与几个能省一半时间的开关
-i是lsof里语法最灵活的参数,也是最容易写错的地方。基础规则是这样:
lsof -i # 所有网络文件(四层协议全部) lsof -i :8080 # 端口 8080,注意冒号不能省 lsof -i TCP:8080 # 只查 TCP 协议的 8080 lsof -i UDP:53 # 只查 UDP 的 53 lsof -i @10.0.0.5 # 只查与这个地址相关的连接(本地或远端) lsof -i -sTCP:LISTEN # 只看处于监听状态的 TCP冒号绝对不能省略。写成lsof -i 8080,8080会被当成服务名去解析,结果很可能是空或者报错,很多人卡在这一步以为是权限问题。
实际使用中我几乎总是加上两个开关:
lsof -nP -iTCP:8080 -sTCP:LISTEN-n禁止把 IP 反解析成主机名,-P禁止把端口号解析成服务名。不加这两个开关,lsof会对每条网络记录做一次反向解析,在 DNS 不可达的环境里可能等上好几秒甚至卡住。这两个开关加起来只多敲四个字符,但能省下大量莫名其妙的等待,我把它们写进了自己的别名里:
alias lsp='lsof -nP -iTCP -sTCP:LISTEN'顺手说下输出里的一个小陷阱。加了-P之后,*:8080会老老实实显示成数字;不加-P,端口可能显示成http-alt这类服务名,你 grep8080就什么都搜不到。这类“为什么明明占用了却查不到”的困惑,多半就是这个原因。
2.3 找到进程之后的分支决策:杀、改端口,还是查为什么没退出
看到 PID 之后,先别急着kill -9。我习惯按这个顺序过一遍:
第一步,确认这个进程是不是“应该存在”的。ps -p <PID> -o pid,ppid,user,etime,cmd看一下启动时间和父进程。如果etime显示好几天,而你的服务应该是今天刚发布的,那它八成是个历史遗留进程。
第二步,确认它的父进程是不是会把它拉起来。如果父进程是 systemd、supervisor 或者某个容器运行时,直接杀掉会立刻重生,而且可能带着一样的问题重生。这时候正确的做法是先停掉对应的管理单元,或者修改配置里的端口。
第三步,看它的打开文件列表里有没有线索。lsof -p <PID> | head -30能看出工作目录、加载的配置文件、监听的端口,基本能判断出这是哪套部署。这个动作比ps更可靠,因为ps里的命令行可能是被人手工改过参数的。
还有一种情况值得单独提:端口被占用但你查不到任何进程。这时候先比对ss -lntp,如果ss也看不到,那大概率是别的网络命名空间里的进程——比如另一个容器占用了同一个宿主机端口,或者某个进程在独立的命名空间里跑的。lsof读的是当前进程能看到的/proc,跨命名空间它是真的无能为力。这不是工具的问题,是边界所在。
-t参数在批量处理时特别顺手,它只输出 PID,非常适合塞进管道:
# 先看清楚再动手,别把两步并成一步 lsof -t -nP -iTCP:8080 -sTCP:LISTEN kill $(lsof -t -nP -iTCP:8080 -sTCP:LISTEN)提示:
kill $(lsof -t ...)这种写法在 PID 为空时会直接报错,不会误杀;但如果端口上有多个监听者,它会一次干掉全部。生产环境上我建议永远先单独执行一次查询,确认结果符合预期再执行 kill。
3. df 说磁盘满了、du 却算不出来:被删除但仍被打开的文件
这是lsof最经典也最容易被神化的用法。先讲清楚原理,再讲操作,否则很容易在错误的场景下套用错误的解法。
3.1 空间不会凭空消失:inode 引用计数与打开句柄
Linux 的文件有两个独立的生命周期:目录项上的名字和数据块的引用。rm做的事只是把目录项摘掉,也就是把 inode 的链接计数(link count)减一。但只要还有进程持有这个文件的句柄,内核就不会释放对应的数据块,因为进程随时可能继续读写。
结果就出现了一个很拧巴的局面:df统计的是文件系统层面的块使用量,它看到数据块还被占着,所以显示满了;du是从目录树往下遍历,它根本看不到这个已经不在目录树里的文件,所以算出来的总和对不上。两个工具都没错,它们回答的是不同的问题。
判断方法非常直接:
df -h /var du -sh /var/* 2>/dev/null | sort -h | tail如果两者差异巨大(比如df说用了 95%,du加起来只有 40%),基本可以确定是这类“幽灵文件”。
3.2+L1与/proc/PID/fd的配合用法
最直接的过滤方式是用+L1,它让lsof只列出链接计数小于 1 的文件,也就是名字已经没了但句柄还在的:
lsof -nP +L1输出里的NLINK列会显示 0,NAME列末尾带(deleted)。我一般会再叠一个按大小排序,快速找出大头:
lsof -nP +L1 2>/dev/null | awk 'NR>1 && $7 ~ /^[0-9]+$/ {print $7, $2, $1, $NF}' | sort -rn | head -20这里有两个实操细节必须说明。第一,+L1需要足够的权限才能在整机范围内看全,普通用户只能看到自己进程的那些。第二,输出的SIZE/OFF列有时候是0t0这种格式(网络文件的偏移量),awk 过滤时要把非纯数字的行排除掉,否则排序会乱。
如果+L1因为某些原因没跑出来(比如版本差异),可以直接走/proc:
# 找出所有进程里指向已删除文件的句柄 ls -l /proc/*/fd/* 2>/dev/null | grep deleted这条命令慢,但胜在只依赖最基本的工具,在极简镜像里也能用。想快速定位到某个具体的大文件,find配合lsof更精准:
lsof -nP 2>/dev/null | grep -i deleted | grep -i '\.log'不过说实话,裸跑全量lsof再 grep 是最费时的方式,它要遍历所有进程的所有句柄,在大机器上能跑几十秒。能用+L1就用+L1,它是让lsof自己在遍历时就把不相关的过滤掉,省掉了整张表打到管道里的开销和内存。
3.3 两种恢复方式:原地截断还是先拷贝再删
定位到具体的/proc/<PID>/fd/<N>之后,处理方式取决于你要不要保住里面的数据。
如果数据没价值,只要把空间还回来,最干净的做法是原地截断:
: > /proc/1204/fd/8这条命令把文件长度清零,句柄不受影响,进程继续往同一个句柄写,空间立刻释放。它比kill -HUP或者重启进程要温和得多,对业务基本无感。
如果数据可能有价值(比如日志里有排查线索),那就先拷贝出来再处理:
cp /proc/1204/fd/8 /tmp/recovered.log : > /proc/1204/fd/8拷贝的时候要注意,如果进程还在不停地写,拷到的不一定是一致快照,但这个场景下足够用了。
还有一种情况要特别小心:如果这个被删文件是一个正在运行的服务的可执行文件本身或者某个动态库,截断它会直接把进程搞崩。所以动手前一定要先确认清楚NAME列指向的是什么类型的文件,TYPE是REG并且路径看起来像二进制或.so的,就不要截断,老老实实重启服务。
提示:截断只对普通文件和日志有效。如果句柄指向的是管道、套接字或者设备,往上面重定向是没意义的,甚至会阻塞住你的 shell。判断依据就是
TYPE列。
根治这类问题的思路才是重点。日志被删了但空间不释放,通常意味着日志轮转配置有问题——比如轮转工具删了旧文件但没给进程发信号让它重开句柄。正确的做法是配置copytruncate模式,或者在轮转后触发一次重开日志的信号。我在一个服务上见过轮转脚本每天准点删文件,而进程从不重开,结果每三天磁盘必满一次,加一行信号发送就彻底消停了。
4. lsof 在生产机器上的性能代价与使用纪律
lsof好用,但它的工作方式是“暴力遍历 + 拼接”,在规模大的机器上有明确的代价。不了解这些代价,很容易在一次紧急排查中把本来就紧张的机器再压垮一根稻草。
4.1 卡住不返回的真实原因:DNS 反查与 stat 阻塞
lsof最常见的“卡死”不是真的挂了,而是卡在等待上。三个来源按概率排序:
DNS 反向解析。不加-n时,每一条网络记录都要做一次地址到主机名的解析。在 DNS 服务器不可达或者响应极慢的环境里,每条记录都要等到超时才继续,几十条连接就能拖出分钟级的等待。这是最典型的场景,解法就是加-n。
端口名解析。不加-P时,lsof会去/etc/services里查端口对应的服务名。这个通常很快,但在某些容器镜像里该文件巨大或者挂载异常,也可能变慢。加-P一并解决。
挂载点 stat 阻塞。这是最隐蔽的一种。lsof需要 stat 各个挂载点来获取文件系统信息,如果机器上挂了一个已经失效的网络文件系统,这次 stat 可能阻塞几十秒甚至永久卡住。这时候用-b参数可以跳过这些阻塞调用,代价是部分信息可能不准:
lsof -b -nP -iTCP -sTCP:LISTEN顺带提一句-S参数可以给可能阻塞的内核调用设置超时秒数,具体支持情况要看版本,不确定的话-b更稳妥。
还有一个纯性能层面的问题:lsof的工作量大致是 O(进程数 × 句柄数)。在一台跑着上百进程、总句柄数几万的机器上,一次全量lsof跑十几秒是正常的。所以我在口径上一直坚持一个原则——能加过滤条件就不要裸跑。lsof -p <PID>、lsof -c <进程名>、lsof -i :<端口>这些都是在遍历时就带着条件,比lsof | grep xxx快一个数量级。
4.2 权限边界、命名空间与容器里的“看不见”
权限规则很简单但经常被忽略:普通用户只能看到自己所属进程的句柄,root才能看到全机。所以当普通用户执行lsof +L1得到空结果时,不要急着下结论说“没有幽灵文件”,换个有权限的账号再看一遍。
内核线程也是看不到的。它们没有用户态的文件句柄,lsof里不会出现,所以“所有进程”这个说法其实只覆盖了用户态进程。
容器环境下的边界更容易踩。lsof读的是/proc,而容器里的/proc是命名空间化的,只能看到本容器内的进程。你在容器里执行lsof -i :8080查不到东西,但宿主机上端口确实被占着,原因就是占用者属于另一个命名空间。这时候要么进对应的命名空间去看,要么直接在宿主机上执行查询。
还有一类常见输出是can't identify protocol。这类记录通常来自那些/proc/net里没有对应条目的套接字,比如某些协议的实现方式不标准。它们不影响使用,但会污染输出,过滤掉就行:
lsof -nP -i 2>/dev/null | grep -v "can't identify protocol"2>/dev/null挡掉的则是那些can't stat()类警告。这些警告在挂载点复杂的机器上几乎必然出现,本身不是错误,但会干扰脚本解析。想彻底关掉可以用-w参数,不过我个人习惯留着2>/dev/null,因为它更有针对性,不会把真正的错误信息一起吞掉。
4.3 一份可以直接复制使用的命令清单
下面这张表是我这几年攒下来的,按场景归类,基本覆盖了日常工作里九成以上的需求:
| 场景 | 命令 | 说明 |
|---|---|---|
| 查端口占用 | lsof -nP -iTCP:8080 -sTCP:LISTEN | 最常用,注意-nP和冒号 |
| 查所有监听端口 | lsof -nP -iTCP -sTCP:LISTEN | 比ss -lntp多显示进程详情 |
| 查某进程打开的套接字 | lsof -nP -p 1204 -a -i | -a是条件“与”,不加会变成“或” |
| 查某个文件被谁打开 | lsof /var/log/app.log | 单文件最快 |
| 查某目录下所有被打开的文件 | lsof +D /data/logs | 慢,慎用 |
| 查已删除但仍被占用 | lsof -nP +L1 | 需足够权限 |
| 查某用户的所有打开文件 | lsof -u appuser | 排查“这个账号到底在跑什么” |
| 查进程的内存映射 | lsof -p 1204 | grep mem | 看加载了哪些动态库 |
| 只输出 PID 供脚本用 | lsof -t -nP -iTCP:8080 | 干净,无表头 |
| 定时刷新观察 | lsof -r 2 -nP -iTCP:8080 | 每 2 秒刷新,直到中断 |
关于-a那个坑必须多说一句。lsof的多个条件默认是或的关系,lsof -p 1204 -i得到的是“PID 1204 打开的文件”加上“所有网络连接”,结果集远大于你的预期。加-a才会变成“PID 1204 打开的网络连接”。这个规则和大多数命令工具的直觉相反,我第一次用的时候也被坑了半天。
同样的道理适用于-u、-c、-d的任意组合。规则是:只要你想让多个条件同时成立,就加-a。
再补充两个替代工具的对照,方便在lsof不可用时切换:
| 工具 | 优势 | 局限 |
|---|---|---|
lsof | 信息最全,覆盖文件、套接字、设备、管道 | 慢,需要遍历/proc |
fuser | 极轻量,专治“谁占着这个文件/端口” | 输出简陋,几乎没有详情 |
ss | 网络连接专用,比lsof快得多 | 只覆盖套接字,不涉及普通文件 |
直接读/proc | 无依赖,最小化镜像里也能用 | 需要自己拼路径,可读性差 |
5. 把 lsof 写进脚本:字段化输出、退出码与反复观察
手工敲命令只能解决一次性的问题。真正能沉淀下来的价值,是把lsof的输出变成脚本可以稳定消费的数据。这里有三样东西值得花时间掌握:-F字段输出、退出码语义、以及重复模式。
5.1 用-F拿机器可读的输出,别再拿 awk 切列
lsof默认的表格输出是给人看的,列宽随内容变化,NAME里还可能含空格,用awk '{print $9}'切出来的结果经常残缺。正确做法是用-F指定需要的字段,输出会变成一行一个字段、每行带单字母前缀的格式:
lsof -F pcn -nP -iTCP:8080 -sTCP:LISTEN输出大概长这样:
p1204 cnginx n*:8080字段字母的含义记住常用的几个就够了:p是 PID,c是命令名,n是名称,f是文件描述符,u是 UID,t是类型,L是登录名。每条记录以p开头,这给了脚本一个天然的分隔符。
解析逻辑很直白:读一行,遇到以p开头就说明新记录开始,遇到其他前缀就把值存进当前记录对应的键,遇到下一条p就把上一条完整记录输出。这个模式用 awk 或 Python 都是十来行的事。相比切列,它不会因为路径里有空格、命令名被截断、或者某列为空而错位。
我在一个巡检脚本里用这个方式统计某个端口上的连接数,跑了两年多没出过解析错误,而早先用切列写的版本每周都要修一次。
5.2 退出码:脚本里判断“有没有”比解析输出更可靠
lsof的退出码语义简单但很多人不知道:
- 找到匹配的打开文件,返回
0 - 没有找到任何匹配项,返回
1 - 执行过程中出错,返回大于
1的值
这意味着判断“端口有没有被占用”根本不需要解析输出,直接看退出码就行:
if lsof -t -nP -iTCP:8080 -sTCP:LISTEN >/dev/null 2>&1; then echo "port 8080 is in use" else echo "port 8080 is free" fi这个写法比lsof ... | grep -q .干净得多,也不会因为grep本身的行为差异(比如pipefail设置)而误判。
要留意的是,权限不足时lsof可能既没报错也没找到东西,返回1。在容器里或者非 root 环境下用这个判断要格外小心,可能得出“端口空闲”的错误结论。所以部署脚本里我只在明确知道自己有足够权限的场景下用它,其余情况还是老老实实看输出。
5.3-r反复观察:抓那些一闪而过的句柄
有些问题不是“当前有没有”,而是“什么时候出现的”。比如某个服务偶尔打开大量临时文件又迅速关闭,等你去查的时候早就没了。-r参数可以让lsof按固定间隔重复执行:
lsof -r 2 -nP -p 1204每两秒刷新一次,Ctrl+C结束。它有两种模式,区别在于什么时候停:
-r:无条件重复,直到被中断+r:一直重复,直到某次没有匹配到任何一个打开文件才退出
+r那个模式在等某个进程彻底退出时特别好用。比如你在做一次优雅停机验证,想确认所有文件句柄都被释放了:
lsof +r 3 -p 1204它会一直刷到进程的句柄表彻底空了才自动退出,比手动反复敲命令可靠。
-r也常被用来观察连接状态的变化。比如排查一条连接为什么反复重建,可以挂着lsof -r 1 -nP -iTCP:8080 -sTCP:ESTABLISHED看几秒,连接数跳变的节奏基本能反映上层的心跳或者重连策略。这个用法比抓包轻量得多,适合先做一个粗筛。
最后分享一个我自己的使用习惯。我把几组常用组合写成了几个短别名,分别对应“查端口”“查打开文件”“查幽灵文件”三类场景,都带上了-nP:
alias lsp='lsof -nP -iTCP -sTCP:LISTEN' alias lsi='lsof -nP -i' alias lsd='lsof -nP +L1 2>/dev/null'这么做的实际收益不是少敲那几个字符,而是避免每次都在紧急情况下漏掉-nP然后对着卡住的终端发呆。紧急排查时人的注意力全在问题上,参数这种细节最容易漏,把它固化进别名,等于给自己加了一层保险。
用久了会发现,lsof真正的价值不在于它有多少参数,而在于它把“进程”和“内核对象”这两侧连成了一张表。凡是需要回答“谁在用这个东西”“这个东西被谁占着”的问题,它基本都是第一站。想清楚这一点,遇到新场景时你不用背命令,顺着“这两侧的关系”去推导,参数自然就想起来了。