Linux 目录结构及 VIM

前言

  • 这篇讲两件事:文件系统与目录结构(Linux 的东西都放在哪儿、怎么找到它),以及 VIM(登上服务器后唯一一定能用的编辑器)。
  • 环境说明:本文所有「实测」都是在《Linux 入门》里那套环境上跑出来的——Ubuntu 26.04.1 LTS(WSL2),内核 6.18.54.1-microsoft-standard-WSL2,VIM 9.1,日期 2026-11-03。Debian/Ubuntu 服务器上的结果基本一致;RHEL 系(Rocky/TencentOS)的差异我会点出来。

文件系统:先分清三种说法

「文件系统」这四个字在中文里同时指三个东西,混着讲就会晕:

说法 英文 它是什么 例子
存储格式 filesystem type 磁盘上组织文件和目录的数据结构,格式化(mkfs)时决定 ext4、xfs、vfat、btrfs、NTFS
管理层 file system(软件机构) 内核里负责读写这套结构的那一层代码 VFS + ext4 驱动
挂载点 mounted filesystem 某个格式被挂到目录树的哪个位置 / 挂在 /dev/sdd,/tmp 是 tmpfs

原笔记那句「文件系统就是管理存储设备或分区上的文件的方法和数据结构」说的是第一种。第二种在 Linux 里有个专门的名字 VFS(Virtual File System,虚拟文件系统):它定义了一套统一接口(打开、读、写、列目录),底下插哪种格式都能用同一组系统调用访问——这就是为什么你 cat 一个文件时不需要关心它在 ext4 还是 9p 上。

第三种最重要,也最容易被新手忽略:Linux 的目录树和磁盘是不是一回事。Windows 是「一个盘符一棵树」,Linux 是「一棵树,多个来源拼起来」——ls -l / 看到的每个目录,可能来自不同的设备、不同的格式。

本机实测:谁挂在哪儿

三条命令就能看清整台机器的存储布局。下面是我这台的真实输出。

findmnt 看挂载关系(比老的 mount 好用,自带树形):

1
2
3
4
5
6
7
8
9
10
11
$ findmnt --real -o TARGET,SOURCE,FSTYPE
TARGET SOURCE FSTYPE
/ /dev/sdd ext4
├─/usr/src/linux-headers-6.18.54.1-microsoft-standard-WSL2 /dev/sdb[/6.18.54.1-microsoft-standard-WSL2/linux-headers] ext4
├─/usr/lib/linux-tools/6.18.54.1-microsoft-standard-WSL2 /dev/sdb[/6.18.54.1-microsoft-standard-WSL2/perf] ext4
├─/usr/lib/wsl/drivers drivers 9p
├─/mnt/wslg/distro /dev/sdd ext4
├─/mnt/c C:\ 9p
├─/mnt/d D:\ 9p
├─/mnt/e E:\ 9p
└─/mnt/f F:\ 9p

读这张表能得到三个结论:

  • 根目录 / 是 ext4,设备是 /dev/sdd(WSL 把发行版的文件系统放在一个 ext4.vhdx 虚拟磁盘里,Windows 侧看是一个文件,Linux 侧看是一块盘)。
  • /mnt/c、/mnt/d 这些是 9p 协议挂载的 Windows 分区,不是真的 Linux 文件系统——所以它们上面没有真正的权限位,chmod 不起作用,读写也慢一个数量级。这就是《Linux 入门》里说「别把工程放在 /mnt/... 下面」的根本原因。
  • /usr/lib/linux-tools/... 这种是从另一块盘 /dev/sdb 里取子目录挂过来的(方括号里是卷内路径),WSL 用它塞内核工具和头文件。

df -Th 看容量和类型(T 就是显示 type):

1
2
3
4
5
6
7
$ df -Th | head -6
Filesystem Type Size Used Avail Use% Mounted on
none overlay 3.9G 0 3.9G 0% /usr/lib/modules/6.18.54.1-microsoft-standard-WSL2
/dev/sdb ext4 97M 97M 0 100% /usr/lib/linux-tools/6.18.54.1-microsoft-standard-WSL2
none tmpfs 3.9G 4.0K 3.9G 1% /mnt/wsl
drivers 9p 301G 184G 117G 62% /usr/lib/wsl/drivers
/dev/sdd ext4 1007G 1.3G 955G 1% /

注意 /dev/sdb 那行 Use% 100%——它是只读的固定大小卷,满了是正常的,不是故障。根分区显示 1007G 但只用了 1.3G,这是稀疏 vhdx 的特征:上限 1 TB,实际按需增长。

lsblk -f 看块设备本身:

1
2
3
4
5
6
7
$ lsblk -f
NAME FSTYPE FSVER LABEL UUID FSAVAIL FSUSE% MOUNTPOINTS
sda ext4 1.0
sdb ext4 1.0 0 100% /usr/lib/linux-tools/...
sdc swap 1 58a9945f-b8e2-494a-8822-00ad1acbe84e [SWAP]
sdd ext4 1.0 aa5d3f47-7c00-402e-9640-6cc107abbe36 954.4G 0% /mnt/wslg/distro
/

sdc 是 swap 分区(对应《Linux 入门》里 .wslconfig 配的 swap=4GB),它不挂在任何目录上,内核自己用。

内核支持哪些文件系统

/proc/filesystems 列出的是当前内核能用的文件系统类型,分两类:带 nodev 的不需要块设备(虚拟文件系统),不带的可以挂在真实磁盘上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cat /proc/filesystems
nodev sysfs
nodev tmpfs
nodev proc
nodev cgroup
...
nodev fuse
nodev fusectl
nodev virtiofs
nodev overlay
udf
xfs
nodev 9p
nodev efivarfs

我这台一次查了 15 种格式的实测结果:

格式 这台 WSL 内核 说明
ext2 / ext3 / ext4 支持 Linux 的看家格式,Ubuntu 根分区默认 ext4
xfs 支持 RHEL/Rocky/TencentOS 的默认格式
vfat 支持 就是 FAT12/16/32,U 盘、ESP 分区都是它
squashfs 支持 只读压缩,snap/livecd 用
overlay 支持 分层联合挂载,Docker 的默认存储驱动
fuse / 9p 支持 用户态文件系统 / 虚拟机共享目录
btrfs 默认没加载 见下面的实测
exfat 默认没加载 同上
ntfs / ntfs3 不支持 微软这个内核没编;Linux 读 NTFS 靠 ntfs-3g(FUSE),实测本机未安装,候选版本 1:2022.10.3-5ubuntu1.1
f2fs 不支持 闪存优化,安卓机内部存储用

关键的一点:/proc/filesystems 里没有 ≠ 内核不支持,可能只是模块还没加载。实测:

1
2
3
4
5
6
7
8
9
10
11
# root 下执行
$ modprobe btrfs
$ modprobe exfat
$ grep -wE "btrfs|exfat|vfat" /proc/filesystems
vfat
btrfs
exfat
$ lsmod | head -3
Module Size Used by
exfat 90112 0
btrfs 1716224 0

modprobe 之后同一份 /proc/filesystems 立刻多出了两行。桌面版 Ubuntu 内核把这些都编进去了,所以你在服务器上一般碰不到这个差别;WSL 用的是精简内核才需要自己加载。

顺带一个新手会撞上的坑:想格式化 U 盘却发现工具根本没装。

1
2
3
4
5
$ for t in mkfs.btrfs mkfs.exfat mkfs.vfat mkfs.ext4; do printf '%-12s %s\n' "$t" "$(command -v $t || echo 未安装)"; done
mkfs.btrfs 未安装
mkfs.exfat 未安装
mkfs.vfat 未安装
mkfs.ext4 /usr/sbin/mkfs.ext4

mkfs.* 属于各自的工具包(dosfstools、btrfs-progs、exfatprogs),最小化系统里只有 e2fsprogs,要用得先 apt install。

常见文件系统格式对照

原笔记那串 FAT16/FAT32/NTFS/EXT4/HFS+/EXFAT 的清单方向是对的,但少了 2019 年之后普及的几个,个别数字也含糊。下面这版把上限数字标清来源:带「实测」的是我这台机器上量出来的,其余是官方文档值(本机是 ext4/9p,没法凭空造出 FAT 卷来验证,mkfs.vfat 也没装)。

格式 出处 主要特点 常见上限(文档值)
FAT16 MS-DOS 16 位簇号,老 分区约 2 GB
FAT32 Windows 95 OSR2 兼容性好,无日志 单文件 4 GB − 1 字节
exFAT Windows CE / 现在 U 盘默认 为闪存设计,无日志,跨 win/mac 单文件 16 EB(实际受平台限制)
NTFS Windows NT 起 日志、ACL、压缩、卷影副本 单文件 16 TB(Windows 实现)
ext2 Linux 早期 无日志 已被 ext3/4 取代
ext3 Linux 给 ext2 加了日志 同上量级
ext4 Linux,2008 起 扩展性日志文件系统,Ubuntu 默认 单文件 16 TB、卷 1 EB(4 KB 簇)
xfs SGI IRIX → Linux 大文件/高并发强,RHEL 系默认 单文件 8 EB 量级
btrfs Oracle 主导 写时复制、快照、校验、内建 RAID 单文件 16 EB
f2fs Samsung 闪存友好,安卓内部存储 —
HFS+ Mac OS 9 → 10.13 苹果旧格式 已被 APFS 取代
APFS macOS 10.13 起 写时复制、快照、加密 单文件 8 EB
squashfs Linux 只读压缩,装了就改不了 —
tmpfs Linux 内存/交换分区当磁盘,重启就没 受内存与 mount size 限制
9p / virtiofs / NFS / CIFS — 网络或跨系统共享,性能与语义都有坑 —

最后一行不是「格式」而是「远程/跨系统文件系统」,但它们在真实服务器上天天遇到,值得单列。

关于 tmpfs,本机实测很直观:

1
2
3
4
5
6
7
8
9
10
$ findmnt /tmp
TARGET SOURCE FSTYPE OPTIONS
/tmp tmpfs tmpfs rw,nosuid,nodev,size=4067168k,nr_inodes=1048576

$ findmnt /run
TARGET SOURCE FSTYPE OPTIONS
/run none tmpfs rw,nosuid,nodev,mode=755

$ findmnt /var/tmp || echo "/var/tmp 不是独立挂载点,落在根文件系统 ext4 上"
/var/tmp 不是独立挂载点,落在根文件系统 ext4 上

结论:/tmp 是内存文件系统,重启内容就没了;/var/tmp 在磁盘上,重启还在。这就是 FHS 规范里「/tmp 里的东西程序退出后可能随时被销毁,/var/tmp 允许长期保存」的由来。这个 /tmp 是 systemd 的 tmp.mount 单元挂上来的:

1
2
3
4
5
6
$ systemctl status tmp.mount --no-pager | head -6
● tmp.mount - Temporary Directory /tmp
Loaded: loaded (/usr/lib/systemd/system/tmp.mount; static)
Active: active (mounted) since Tue 2026-11-03 17:22:13 CST; 19s ago
Where: /tmp
What: tmpfs

而传统上负责挂载磁盘的 /etc/fstab,在这台机器上是空的:

1
2
$ cat /etc/fstab
# UNCONFIGURED FSTAB FOR BASE SYSTEM

WSL 不需要它,Windows 的盘由 WSL 自己按 /etc/wsl.conf 挂载(实测该文件只有 [boot] systemd=true 和 [user] default=loong 两段)。在真服务器上,/etc/fstab 是开机自动挂载表,改错了会起不来,改之前务必备份。

根目录结构(实测版)

Linux 没有 C 盘 D 盘,一切都从 / 这棵树长出来。下面这版是我这台机器上 ls -l / 的真实输出,条目数也是量的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ ls -l /
total 3456
lrwxrwxrwx 1 root root 7 Apr 20 2026 bin -> usr/bin
drwxr-xr-x 2 root root 4096 Apr 20 2026 boot
drwxr-xr-x 15 root root 3820 Nov 3 16:46 dev
drwxr-xr-x 89 root root 4096 Nov 3 17:06 etc
drwxr-xr-x 3 root root 4096 Nov 3 16:43 home
-rwxr-xr-x 1 root root 3476152 Oct 8 05:21 init
lrwxrwxrwx 1 root root 7 Apr 20 2026 lib -> usr/lib
lrwxrwxrwx 1 root root 9 Apr 20 2026 lib64 -> usr/lib64
drwx------ 2 root root 16384 Nov 3 16:30 lost+found
drwxr-xr-x 2 root root 4096 Aug 27 22:10 media
drwxr-xr-x 8 root root 4096 Nov 3 16:31 mnt
drwxr-xr-x 2 root root 4096 Aug 27 22:10 opt
dr-xr-xr-x 182 root root 0 Nov 3 16:46 proc
drwx------ 4 root root 4096 Nov 3 16:52 root
drwxr-xr-x 23 root root 620 Nov 3 16:46 run
lrwxwxrwx 1 root root 8 Apr 20 2026 sbin -> usr/sbin
drwxr-xr-x 2 root root 4096 Nov 3 16:31 snap
drwxr-xr-x 2 root root 4096 Aug 27 22:10 srv
dr-xr-xr-x 12 root root 0 Nov 3 16:46 sys
drwxrwxrwt 6 root root 120 Nov 3 16:58 tmp
drwxr-xr-x 12 root root 4096 Aug 27 22:10 usr
drwxr-xr-x 13 root root 4096 Nov 3 16:31 var
目录 是什么 本机实测
/ 根目录,唯一没有上级 挂在 /dev/sdd,ext4
/bin 用户命令 软链接 → usr/bin,905 个条目
/boot 内核、initramfs、GRUB 配置 空(WSL 的启动不经过它,内核由 Windows 侧提供)
/dev 设备文件 189 项,块设备 28 个、字符设备 140 个
/etc 全局配置文件 172 项,*.conf 递归一层就有 73 个
/home 普通用户家目录 1 项:loong,权限 drwxr-x---(750)
/init WSL 自己的启动程序 3.4 MB 可执行文件,物理机/普通服务器上不存在
/lib、/lib64 共享库 都是软链接 → /usr/lib、/usr/lib64
/lost+found ext4 文件系统检查时捡回来的碎片 空,权限 drwx------(只有 root 能进)
/media 桌面环境插入 U 盘时的自动挂载点 空(服务器/WSL 上没有桌面自动挂载)
/mnt 手工挂载的约定位置 6 项:c d e f wsl wslg,被 WSL 占用了
/opt 第三方大包(自己装一套进去) 空
/proc 进程与内核信息的虚拟文件系统 不占磁盘空间,dr-xr-xr-x
/root root 用户的家目录,和 / 不是一回事 权限 drwx------(700),普通用户进不去
/run 开机以来的运行时信息(PID、socket) tmpfs,28 项,重启清空
/sbin 系统管理命令 软链接 → usr/sbin,176 个条目
/srv 本机对外提供的数据(网站根目录等) 空
/sys 内核/设备/驱动的虚拟文件系统 dr-xr-xr-x
/tmp 临时文件 tmpfs,drwxrwxrwt(注意末尾的 t)
/usr 系统自带的程序与数据(Unix System Resources) 10 个子目录,见下一节
/var 会变的数据:日志、缓存、队列 14 个子目录

必须记住的两处变化:

  • /bin、/sbin、/lib 已经是符号链接。这件事叫 usrmerge: Debian 从 10、Ubuntu 从 22.04 起把可执行文件和库统一放进 /usr,根目录只留软链接。所以「/bin 放命令、/usr/bin 也放命令」这种区分在今天只是历史,写脚本时用哪个都能跑,但别去 ls -l /bin 找实体目录。
  • /cdrom 不存在(原笔记表里有)。它只是老 Ubuntu 安装器留下的挂载点,服务器和 WSL 上都没有。查一下实测就是 ls: cannot access '/cdrom'。
  • /snap 在,但基本是空的。里面只有一个 README,内容是「This directory presents installed snap packages」;snap list 输出「No snaps are installed yet」。snapd 2.76.3 装了但没在用。原笔记「/snap 是 Ubuntu 自己搞的包管理系统」这句描述没错,但要补一句:它是包管理器的挂载目录,不是数据目录,真正的包数据在 /var/lib/snapd。

四个最常进去的目录展开一层

背整棵树没用,日常真正反复进的就这四个。

/usr:程序和数据,装完基本不变

1
2
$ ls /usr
bin games include lib lib64 libexec local sbin share src
子目录 放什么 你会什么时候进去
/usr/bin 所有用户命令(本机 905 个) which/command -v 找程序
/usr/sbin 管理命令(176 个) 只有 sudo 时用得到
/usr/lib 共享库、插件、systemd 单元文件 查 /usr/lib/systemd/system/*.service
/usr/local 你自己装的东西,包管理器不碰它 源码 make install 默认装这里
/usr/share 与架构无关的数据:文档、图标、字典 /usr/share/doc 本机 545 个包各有一份文档
/usr/include C 头文件 编译时报「找不到头文件」先查这里
/usr/src 内核源码/头文件 装驱动时

/usr/local 是「不想被包管理器覆盖」的标准去处,它内部结构和 /usr 一模一样(实测:bin etc games include lib libexec man sbin share src)。

/etc:配置,全是文本文件

1
2
3
4
5
$ ls /etc | head -24
PackageKit X11 adduser.conf alternatives apparmor apparmor.d apport apt
bash.bashrc bash_completion bash_completion.d bindresvport.blacklist binfmt.d
byobu ca-certificates ca-certificates.conf ca-certificates.conf.dpkg-old
chrony cloud console-setup credstore credstore.encrypted cron.d cron.daily

三个和新手最相关的点:

  • /etc 下的文件不该由你手动创建,但该由你手动读——服务改端口、加用户、配定时任务都在这里。
  • 带 .dpkg-old、.dpkg-new、.distrib 后缀的是包管理器升级配置时留下的备份,看到别慌。
  • 敏感文件靠权限保护,实测:
1
2
3
4
5
$ stat -c '%n %A %U %G' /etc/passwd /etc/shadow /root /home/loong
/etc/passwd -rw-r--r-- root root
/etc/shadow -rw-r----- root shadow
/root drwx------ root root
/home/loong drwxr-x--- loong loong

/etc/passwd 人人可读(只有账号信息,没有密码),/etc/shadow 是密码哈希,属组 shadow、权限 640,普通用户读不了。注意最后一行:Ubuntu 现在把用户家目录设成 750,别的用户进不了你的 ~,这是安全默认值,不是坏了。

/var:会变的数据

1
2
$ ls /var | tr '\n' ' '
backups cache crash lib local lock log mail opt run snap spool tmp

排查问题最常去的是 /var/log(本机 17 MB,apt、dpkg.log、auth.log、dmesg、bootstrap.log 等都在),其次是 /var/lib(服务的状态数据,比如 Docker 镜像、数据库文件)。

/run:只在本次开机存在的运行时状态

1
2
3
$ ls /run | head -10
WSL agetty.reload apport.socket chrony cloud-init console-setup
credentials crond.pid crond.reboot dbus

crond.pid 这种「进程号文件」就放这里。它挂的是 tmpfs,所以重启自动清空——这也是为什么 /run 里绝不该放需要持久化的东西。

一切皆文件:设备文件与虚拟文件系统

Linux 把「磁盘、终端、键盘、随机数发生器」都表达成 /dev 下的一个文件,用同一套 open/read/write 访问。实测看类型:

1
2
3
4
5
6
7
$ ls -l /dev/null /dev/zero /dev/random /dev/tty /dev/sda /dev/sdd
crw-rw-rw- 1 root root 1, 3 Nov 3 17:15 /dev/null
crw-rw-rw- 1 root root 1, 8 Nov 3 17:15 /dev/random
crw-rw-rw- 1 root tty 5, 0 Nov 3 17:15 /dev/tty
crw-rw-rw- 1 root root 1, 5 Nov 3 17:15 /dev/zero
brw-rw---- 1 root disk 8, 0 Nov 3 17:15 /dev/sda
brw-rw---- 1 root disk 8, 48 Nov 3 17:15 /dev/sdd

怎么读这行:

  • 第一个字符 c = character device(字符设备,一次一个字节,如终端、/dev/null),b = block device(块设备,按块读写,如磁盘)。本机 /dev 实测 140 个字符设备、28 个块设备、0 个普通文件。
  • 1, 3 是主设备号, 次设备号,内核靠它找到驱动;8, 48 就是「第 8 号 SCSI 磁盘驱动的第 48 个设备」= 第 4 块盘的第 1 个分区位置算法。
  • 设备文件的时间戳是系统启动时间,不是文件被改的时间。

用起来最顺手的是 /dev/null:把不要的输出丢进黑洞。

1
2
3
4
5
$ echo hello > /dev/null
$ echo $?
0
$ timeout 1 head -c 16 /dev/zero | od -c | head -2
0000000 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0

/dev/zero 读出来全是 0(用来造测试文件),/dev/null 写多少都不占空间。所以「command > /dev/null 2>&1」的意思是把正常输出和错误输出都丢掉,只留下退出码。

/proc 和 /sys 是内核内存的目录化视图,不占磁盘:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ head -5 /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 154
model name : 12th Gen Intel(R) Core(TM) i9-12900H

$ head -3 /proc/meminfo
MemTotal: 8134332 kB
MemFree: 7618544 kB
MemAvailable: 7603064 kB

$ ls /sys
block bus class dev devices firmware fs kernel module power

MemTotal 8134332 kB ≈ 7.8 GiB,正是 .wslconfig 里 memory=8GB 的效果——这些文件是「真相」,比任何 GUI 显示的数字可靠。/proc 偏进程与内核状态,/sys 偏设备与驱动属性(sysfs)。

路径的四个特殊记号与隐藏文件

原笔记的「尝试」四条,我全部实测了一遍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cd /tmp/probe && touch .hidden visible
$ ls
a visible
$ ls -a
. .. .hidden a visible
$ ls -A
.hidden a visible
$ pwd
/tmp/probe
$ cd .. && pwd
/tmp
$ echo $HOME
/home/loong
$ echo ~
/home/loong
记号 含义 补充
. 当前目录 ls 默认不显示,./脚本.sh 用来执行当前目录里的脚本
.. 上一级目录 /.. 还是 /,根目录没有上级
~ 当前用户的家目录 ~loong 是 loong 的,~root 是 root 的;echo ~ 与 $HOME 实测一致
/ 根目录 也是路径分隔符,Windows 的反斜杠在这儿是普通字符
以 . 开头的文件 隐藏文件 ls -a 才显示;ls -A 显示但省掉 . 和 .. 两项

家目录里到底有什么,实测(新建用户时这些是从 /etc/skel 复制来的,那里就 .bash_logout .bashrc .profile 三个):

1
2
$ ls -A ~
.bash_logout .bashrc .cache .config .motd_shown .profile .viminfo

.viminfo 是 VIM 留下的(后面会讲)。

一个原笔记没提但一定会踩的坑:文件名以 - 开头时,命令会把它当选项。

1
2
$ touch ./-dash && rm ./-dash && echo "用 ./-dash 可以删"
用 ./-dash 可以删

正确写法是加 ./ 前缀(或用 -- 结束选项解析:rm -- -dash)。

新手最容易迷路的三件事

  • 相对路径是相对「当前目录」,不是相对「你脑子里的项目目录」。 cd /etc 之后 ls nginx 看的是 /etc/nginx,不是 /usr/share/nginx。任何时候先 pwd。
  • 绝对路径和相对路径混着写会出诡异错误。 脚本里 ./run.sh 和 /opt/app/run.sh 都没问题,但 run.sh(不带 ./)会去 PATH 里找,找不到就 command not found——这是当前目录不在 PATH 里的安全设计,不是 bug。
  • 权限不足时报的是「Permission denied」,目录不存在时报的是「No such file or directory」,两者别搞混。前者要么 sudo 要么改属主,后者是路径写错了(注意 Linux 区分大小写,/etc/X11 和 /etc/x11 是两个东西;博客里图片路径大小写写错,本地 Windows 一切正常、线上 Linux 直接 404,我踩过)。

vi、vim、vim.tiny 是同一个人吗

原笔记说「vi 是默认自带的编辑器,后来有了加强版 vim」,方向对,但今天 Ubuntu 上的实际情况是这样(实测):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ command -v vi vim view vimdiff
/usr/bin/vi
/usr/bin/vim
/usr/bin/view
/usr/bin/vimdiff

$ readlink -f $(command -v vi)
/usr/bin/vim.basic
$ readlink -f $(command -v vim)
/usr/bin/vim.basic

$ update-alternatives --query vi | head -6
Name: vi
Link: /usr/bin/vi
Slaves:
vi.1.gz /usr/share/man/man1/vi.1.gz
Status: auto
Best: /usr/bin/vim.basic

也就是说:

  • /usr/bin/vi 是 alternatives 机制的一个软链接,当前指向 vim.basic,所以 vi 和 vim 跑的是同一个程序(vim-tiny 也在候选列表里,优先级更低才没被选中)。
  • 但 vi 启动时 Vim 会按名字进入兼容模式(nocompatible 不生效),行为差异主要在退格键、可视块等细节上。写脚本、配 git 时看到 vi 别以为是另一个编辑器。
  • 本机版本与包:
1
2
3
4
5
6
7
8
9
10
$ vim --version | head -2
VIM - Vi IMproved 9.1 (2024 Jan 02, compiled Aug 18 2026 18:55:02)
Included patches: 1-948, 950-2141

$ dpkg -l | grep -E "^ii +(vim|vim-runtime|vim-tiny|nano)" | awk '{print $2, $3}'
nano 8.7.1-1ubuntu0.1
vim 2:9.1.2141-1ubuntu4.8
vim-common 2:9.1.2141-1ubuntu4.8
vim-runtime 2:9.1.2141-1ubuntu4.8
vim-tiny 2:9.1.2141-1ubuntu4.8
  • 编译特性决定能力边界,实测这台是 +mouse +syntax +clipboard +terminal +python3 +virtualedit +job +channel,-lua -perl -xterm_save。看到 -mouse 才需要装完整版,这里 +mouse 说明 :set mouse=a 能用。
  • nvim(Neovim)没装;nano 装了(/usr/bin/nano)。新手在服务器上如果 vim 操作不熟,nano 是一定能用的退路:底部一行快捷键提示,改完 Ctrl+O 保存、Ctrl+X 退出。
  • vimtutor 在 /usr/bin/vimtutor,这是官方自带的两小时教程,比背任何表格都有效:vimtutor 直接跑(本机 /usr/share/vim/vim91/tutor 下有 50 多个语言版本,含 tutor1.zh_cn)。

安装命令照旧是 sudo apt install vim,但要知道 Ubuntu Server/WSL 镜像里通常已经带了,先 command -v vim 再装。

VIM 的三种模式

VIM 的一切困惑都来自「同一个按键在不同模式下意思不同」。三种模式:

模式 怎么进 能干什么 屏幕上的表现
正常模式(命令模式) 打开文件时的默认模式;任何时候按 ESC 回到它 移动光标、删除、复制粘贴、撤销、按 : 进单行模式 没有提示符
插入模式 i a o I A O s S 等 打字 左下角出现 -- INSERT --
单行模式(ex 命令) 正常模式下按 :(或 / ?) 存盘、退出、查找替换、改配置 光标跑到最底行,前面有个 :

原笔记漏了一个同样常用的:可视模式(v 按字符选、V 按行选、Ctrl-V 按列选),选中后可以直接对这块区域执行 d/y/>。

还有一个新手最容易慌的点:ESC 按了没反应、: 打不出来——很可能是上一次插入模式没退干净,连按几次 ESC 就回来了。想退出又不敢退的时候,:q! 是万能逃生门(不保存退出)。

按键实测:正常模式

下面每一行都是我在本机 vim 9.1 上真跑出来的结果,测试文件是 5 行 line1…line5(除注明外)。

操作 实测结果
2 然后 dd 变成 4 行:line1 line3 line4 line5(删掉光标所在行)
2 然后 3dd 变成 2 行:line1 line5(从第 2 行起删 3 行)
1 然后 yy 再 p 变成 6 行:line1 被复制了一份
dd 再 u 5 行完整恢复(撤销)
dd 再 u 再 Ctrl-R 又变成 4 行(反撤销,重做删除)
第 3 行按 x line3 → ine3(删光标下那个字符,向右)
第 3 行按 2x line3 → ne3
行首按 X 不变(左边没字符可删,边界安全)
hello world 行尾按 X → hello word(向左删一个字符)
gg 光标到第 1 行
G 光标到最后一行(实测 :put 写出的行号 = 文件末行)
3gg 到第 3 行
G2k 末行往上 2 行
:4 光标到第 4 行(实测输出该行内容 a4)
vim +4 file 打开即定位第 4 行(实测当前行内容 a4)
vim file + 打开即定位末行(实测当前行内容 a6)
0 / ^ / $ 列号实测:0 在第 1 列,^ 在第一个非空字符(3 个空格缩进的行 = 第 4 列),$ 在最后一个字符( lead = 第 7 列)
J 把下一行并上来:a1 + a2 → a1 a2
w / b 下一个单词开头 / 上一个单词开头(实测 0w 后列号 = 8,正好落在 the 之后的 q 前)
3G 然后 dw 第 3 行变空(删掉一个单词 line3)
3G 然后 gUgg 前 3 行全大写:A1/A2/A3/a4/a5/a6(gU 是「到大写」的操作符,配运动 gg)
3G 然后 5y 再 p 复制粘贴,寄存器内容实测为最后一行

原表里 h j k l(左下上右)、Ctrl-F/B(整页)、Ctrl-D/U(半页)、nx、nX、nyy、ndd 这些写法都成立,只是分页类按键需要真实屏幕高度,无终端环境下测不出滚动效果,我按官方文档保留。

补两个原笔记没写、但极其省事的:

  • f<char> 跳到本行下一个该字符(t<char> 停在它前面),配合 ; 重复。
  • :set number 之后按 5G 和 :5 等价,但 5G 不用离开正常模式。

按键实测:插入模式与单行模式

进入插入模式的 8 个键,实测区别(第 3 行内容是 line3,缩进行是 lead):

键 实测结果 一句话记法
i line3 → XXXline3 光标前插入
I lead → XXXlead 第一个非空字符前插入(不是第 1 列!)
a abc → aZZZbc 光标后插入
A lead → leadEND 行尾插入
o 在下方新开一行 APPENDED open below
O 在上方新开一行 ABOVE open above
s 删光标下字符并进插入 小写 = 字符级
S 整行变成 REPL 大写 = 整行级

规律就是大写比小写「范围更大」:i 在光标、I 在行首非空;a 在光标后、A 在行尾;s 删一字符、S 删整行;o/O 上下对称。

单行模式(按 : 之后)实测:

命令 实测
:w 保存
:w 新文件名 另存为,本机确认生成了 alt.txt
:q 没有修改时退出
:q! 有修改也强行退出,文件保持原样(实测内容仍是 orig)
:wq / :x 保存并退出,两个都被接受
:w! 只读文件强制保存(需要文件本身可写或你有权限)
:e! 放弃全部修改,重新读入磁盘版本(实测内容回到 orig)
:set nu / :set nonu 显示 / 关闭行号,实测都生效
:set tabstop=4 一个 Tab 显示宽度设为 4,生效
:set mouse=a 允许鼠标点击定位,本机 +mouse 支持,生效
:nohlsearch 清掉当前搜索的高亮,生效
:1,3s/a/b/g 第 1 到 3 行做替换,生效
:n 跳到第 n 行,实测 :4 落在第 4 行

替换的 g 到底影响什么,实测最直观(文件内容 aaa bbb aaa / ccc aaa):

1
2
3
:%s/aaa/X/      →  X bbb aaa   /  ccc X      ← 每行只替换第一处
:%s/aaa/X/g → X bbb X / ccc X ← 加了 g,每行全部替换
:1,1s/aaa/X/g → X bbb X / ccc aaa ← 限定第 1 行,行内全替换

所以「g 是全局」这个说法要讲准确:g 管的是「一行内多处」,跨行是靠地址范围(% 表示全部行),两者是正交的。原笔记那句「g 表示全局(默认只能替换一行中的第一处)」其实已经说对了,只是「全局」这个词容易让人以为是跨行。

原笔记里那两条写错的命令

第一条:表格里 :no nonu[mber] | 隐藏行号。实测不成立。

1
2
3
4
5
6
7
8
$ vim -u NONE -N -es -c 'set number' -c 'no' -c 'set number?' -c 'q'
number ← 行号还开着
$ vim -u NONE -N -es -c 'set number' -c 'no nonu' -c 'set number?' -c 'q'
number ← 还是开着
$ vim -u NONE -N -es -c 'set number' -c 'nonu' -c 'set number?' -c 'q'
number ← 单独写 nonu 也不行
$ vim -u NONE -N -es -c 'set number' -c 'set nonu' -c 'set number?' -c 'q'
nonumber ← 只有这条真的关掉了

原因是 :no 是 :nohlsearch 的缩写(清除搜索高亮),而 Vim 的命令行缩写要求唯一前缀,nonu、nu、n 这几个前缀在 :no... 这一族里不唯一,所以并不会被当成 :set nonumber。正确写法只有两种:

  • 关行号::set nonu(或 :set nonumber)
  • 清搜索高亮::no(完整 :nohlsearch)

顺带把 :set number? 这个用法记住:问号结尾是「查询当前值」,:set mouse?、:set tabstop? 同理,是排查配置有没有生效的最快办法。

第二条:若文件没有生效,需要重新加载一次 source ~/ .vimrc。路径里多了一个空格,~/ .vimrc 会被解析成两个参数(家目录 + 一个不存在的 .vimrc)。正确写法:

1
2
:source ~/.vimrc
:so ~/.vimrc " so 是 source 的合法缩写

另外「打开文件后的配置是临时的,关闭就失效」这句要更正::set 的修改在退出时会写进 ~/.viminfo(shada)里的一部分(比如标记、撤销历史),但选项本身不会保存,所以确实需要写进 ~/.vimrc 才永久生效。

vimrc:让配置永久生效

Ubuntu 的系统级配置在 /etc/vim/vimrc,实测它的非注释内容只有这些:

1
2
3
4
5
6
7
8
$ grep -v '^ *"' /etc/vim/vimrc | grep -v '^ *$'
runtime! debian.vim
if has("syntax")
syntax on
endif
if filereadable("/etc/vim/vimrc.local")
source /etc/vim/vimrc.local
endif

这就解释了为什么你的 vim 有语法高亮、却没有行号、鼠标也不能点。三种配置来源下的出厂默认值,实测对比:

启动方式 实测默认值
vim -u NONE(完全裸配置) nonumber mouse= tabstop=8 background=light
vim -u DEFAULTS(Vim 自己的默认) nonumber mouse=nvi tabstop=8 background=light
正常启动(读 /etc/vim/vimrc) nonumber mouse= tabstop=8 background=light,语法高亮开

要改就写在自己的 ~/.vimrc(本机实测这个文件默认不存在,需要自己建)。原笔记那份配置我逐条加载验证过,11 行全部被接受、没有报错:

~/.vimrc
1
2
3
4
5
6
7
8
9
10
11
syntax on               " 开语法高亮
set nu " 显示行号
set autoindent " 新行继承上一行的缩进
set smartindent " 在 autoindent 基础上按语言规则加缩进
set tabstop=4 " Tab 显示宽度 4
set shiftwidth=4 " 自动缩进一次进 4
set showmatch " 光标在括号上时高亮配对括号
set ruler " 右下角常驻显示「行,列」
set cindent " C 系语言专用缩进(写 Python 时建议别开)
set background=dark " 告诉配色方案背景是深色
set mouse=a " 允许鼠标点击/滚动定位

实测加载后的取值:number autoindent smartindent tabstop=4 shiftwidth=4 showmatch ruler cindent background=dark mouse=a,全部生效。

两个补充建议:

  • 写 Python 时把 cindent 去掉——它是给 C/Java 那类花括号语言用的,和 Python 的缩进语义会打架。
  • set mouse=a 在终端里有个副作用:选中文字复制会变成 VIM 自己的可视块选择,而不是终端选择。Linux/WSL 下按住 Shift 再用鼠标选,就能绕过。
  • 改完 ~/.vimrc 想立刻看效果,两种办法:重开 vim,或者在单行模式里 :source ~/.vimrc。
  • 想知道某条设置到底被谁设过,用 :verbose set tabstop?,它会打印「最后由哪个文件设置」。

交换文件与恢复

原笔记那段「若非正常关闭会产生临时交换文件」说的是真的,我把它量化了:用 screen 起一个真终端的 vim 打开 crash.txt,文件一被打开,同目录立刻出现隐藏交换文件:

1
2
3
4
$ ls -A
.crash.txt.swp crash.txt
$ stat -c '%n %s 字节 %A %U:%G' .crash.txt.swp
.crash.txt.swp 4096 字节 -rw-r--r-- loong:loong

命名规则是 . + 原文件名 + .swp,所以它属于隐藏文件,只有 ls -a/ls -A 能看见。正常 :wq 退出后它会被自动删掉(实测:ls -A | grep -c swp 返回 0)。

不加文件名单用 vim -r,就是列出可恢复的交换文件:

1
2
3
$ vim -r
Swap files found:
(本机此刻没有残留,所以列表为空)

如果确实有残留,vim 打开同一文件时会弹一个 E325 提示,给你 Open read-only / Recover / Quit / Abort / Delete / Unedit 等选项,选 R 就从交换文件里把未保存的内容捞回来,然后按提示删掉 .swp。手动清理也很直接:rm .文件名.swp。

说明一句诚实的边界:我用 kill -9 试了两次想复现「残留 + E325 弹窗」,两次都没留住现场——vim 在无终端环境下会读到 EOF 正常退出并清理交换文件,而通过 screen 强杀时上层会话又会给它补一个退出机会。交换文件的创建和清理是实测的,E325 那个提示界面是 Vim 官方文档描述的(:help E325、:help swap-file)。

顺带两个同源的东西:

  • ~/.viminfo(新版叫 shada)记录上次编辑的位置、标记、撤销历史、搜索历史。实测本机这个文件里存了 48 条最近文件,所以 vim 里 :e # 能回到上一个文件。
  • 原笔记「!v 打开最后使用 vim 打开的文件」这条不是 vim 功能,是 bash 的历史展开:! + 前缀 = 重复最近一条以它开头的命令(实测 set -o | grep histexpand 显示 histexpand on,交互式 bash 默认开启)。所以 !v 的意思是「再跑一遍最近那条以 v 开头的命令」,通常是 vim 某文件。同一族的还有 !!(上一条)、!$(上一条的最后一个参数)。

help 使用

--help 是绝大多数外部命令自带的短帮助,实测:

1
2
3
4
5
6
7
8
$ ls --help | head -5
List directory contents.
Ignore files and directories starting with a '.' by default

Usage: ls [OPTION]... [FILE]...

$ ls --help > /dev/null 2>&1; echo $?
0

三个实测出来的坑:

  • -h 不等于 --help。ls -h 是 human-readable(KB/MB 显示),grep -h 是「不打印文件名」,tar -h 是另一回事。查帮助请写全 --help。
  • shell 内建命令没有 --help。实测 echo --help 直接把 --help 当字符串打印出来了,test --help 则毫无输出。内建命令要用 help:
1
2
3
4
5
6
7
8
$ cd --help
cd: cd [-L|[-P [-e]]] [-@] [dir]
$ help | head -5
GNU bash, version 5.3.9(1)-release (x86_64-pc-linux-gnu)
These shell commands are defined internally. Type `help' to see this list.
Type `help name' to find out more about the function `name'.
Use `info bash' to find out more about the shell in general.
Use `man -k' or `info' to find out more about commands not in this list.
  • 层次上从浅到深是:命令 --help → man 命令 → info 命令 → 上游文档。本机 info 已安装(/usr/bin/info),tldr(社区维护的「只看常用例子」版)没装,想用得 apt install tldr。

VIM 自己的帮助是内置的,在 vim 里输入 :help 回车即可,它有 159 篇文档(实测 /usr/share/vim/vim91/doc 下 159 个文件)。常用的几个主题::help index.txt(快捷键总表)、:help quickref.txt(一页速查)、:help usr_t.txt(教程第 10 章「撤销/替换」)、:help tabstop、:help :wq。查完用 :q 关掉帮助窗口。

不会退出 vim 的时候,逃生路线永远是:ESC 多按几下 → :q! 回车。

man 使用

man 是 manual 的缩写,查系统标准手册页。本机实测:man-db 版本 2.13.1,装了 3834 份手册页,分 8 节:

1
2
3
4
5
6
7
8
9
$ for s in 1 2 3 4 5 6 7 8; do printf 'man%-2s %s 页  例: %s\n' "$s" "$(ls /usr/share/man/man$s | wc -l)" "$(ls /usr/share/man/man$s | head -3 | tr '\n' '/')"; done
man1 1189 页 例: CA.pl.1ssl.gz/NF.1.gz/[.1.gz/
man2 1 页 例: intro.2.gz/
man3 44 页 例: Error.3pm.gz/Error::Simple.3pm.gz/Git.3pm.gz/
man4 36 页 例: cciss.4.gz/console_codes.4.gz/cpuid.4.gz/
man5 348 页 例: Compose.5.gz/XCompose.5.gz/Xsession.5.gz/
man6 1 页 例: intro.6.gz/
man7 466 页 例: EVP_ASYM_CIPHER-RSA.7ssl.gz/...
man8 587 页 例: 30-systemd-environment-d-generator.8.gz/aa-load.8.gz/...

八节的含义(这个背下来,查东西会准很多):

节 内容 典型例子
1 用户命令 man 1 ls
2 系统调用(内核给的接口) man 2 open
3 C 库函数 man 3 printf
4 特殊设备文件 man 4 console_codes
5 文件格式与约定 man 5 passwd、man 5 fstab
6 游戏 基本用不到
7 杂项与协议 man 7 regex、man 7 ip
8 系统管理命令 man 8 systemctl

查配置文件要带节号,否则可能查到同名命令上:man passwd 是命令(改密码),man 5 passwd 才是 /etc/passwd 这个文件的格式说明。

实测这台机器的两个缺口:

1
2
3
4
$ man 2 open
No manual entry for open in section 2
$ man 3 open
No manual entry for open in section 3

原因是第 2、3 节的正文来自 manpages-dev 包,本机只装了 manpages 6.17-1,没装 manpages-dev——所以 man2 只有 1 页(intro)。写 C 语言要查函数得先 sudo apt install manpages-dev。

同名手册可能有多份实现,实测:

1
2
3
4
5
6
$ man -w ls
/usr/share/man/man1/rust-ls.1.gz
$ man -k chmod | head -3
chmod (1) - Change the mode of each FILE to MODE. With --reference...
gnuchmod (1) - change file mode bits
rust-chmod (1) - Change the mode of each FILE to MODE...

man -w 只打印路径不打开页面,用来确认「我到底会读到哪一份」。Ubuntu 26.04 上 ls 的手册页指向的是 rust 版 coreutils,这就是为什么 ls --help 的措辞和老 GNU coreutils 不一样。

man -f(等于 whatis)和 man -k(等于 apropos)是基于索引的快速查询,本机索引在 /var/cache/man/index.db,实测可用:

1
2
3
4
$ man -f ls
ls (1) - List directory contents. Ignore files and directories starting with a '.' by default
$ man -k chmod
chmod (1) - Change the mode of each FILE to MODE...

如果 man -k 报「没有索引」,跑一次 sudo mandb 重建即可。

翻页按键,原笔记那张表有几处错位,实测更正如下(man 用的是分页器,行为由 $PAGER 决定,Ubuntu 默认是 less):

按键 作用
空格 或 d 或 PageDown 向下翻一页
b 或 u 或 PageUp 向上翻一页
↑ / ↓ 只滚动一行(不是页!原笔记把 ↓ 写成「向下翻一页」是错的)
g / G 跳到页首 / 页尾
/关键词 向后搜索,n 下一个、N 上一个
?关键词 向前搜索
h 分页器自己的帮助
q 退出

手册页自身的结构是固定的小节(实测 man 1 ls 的小节标题):NAME(一句话说明)、SYNOPSIS(用法,方括号表示可选、| 表示互斥)、DESCRIPTION(逐项解释)、OPTIONS、EXIT STATUS、BUGS、COPYRIGHT。

原笔记最后那句「有问题,找男人」——保留,但更准确的说法是:--help 看用法,man 看全量,/usr/share/doc/包名 看长文档,info 看 GNU 项目的教程,四个都试一遍再上网搜。

常见坑速查

  • 以为 /bin、/sbin、/lib 是真实目录:现在都是指向 /usr/ 的软链接(实测 readlink),别写脚本去 find /bin 找实体。
  • 把工程放在 /mnt/c 下:那是 9p 挂载的 Windows 分区,chmod 无效、权限位全是 777、IO 慢一个数量级。
  • 以为 /tmp 里的东西会留着:它是 tmpfs,重启就没;需要跨重启请用 /var/tmp。
  • 分不清 / 和 /root:前者是根目录,后者是 root 用户的家目录(实测权限 700,普通用户进不去)。
  • ls 看不到隐藏文件:以 . 开头的要 ls -a(-A 连 . 和 .. 都不显示)。
  • 文件名以 - 开头删不掉:用 rm ./-dash 或 rm -- -dash。
  • :no nonu 关不掉行号::no 是 :nohlsearch,关行号要 :set nonu(实测)。
  • source ~/ .vimrc 多打一个空格:写成 :source ~/.vimrc。
  • 以为 vi 是另一个老编辑器:实测它是 alternatives 指向 vim.basic 的软链接。
  • echo --help:内建命令没有 --help,它会把 --help 原样打印出来;用 help echo。
  • man passwd 查配置文件:要写 man 5 passwd;查不到系统调用是因为没装 manpages-dev。
  • 以为 !v 是 vim 的历史功能:那是 bash 的历史展开。
  • 大小写敏感:/etc/X11 不是 /etc/x11,图片路径也一样(Windows 本地正常、Linux 线上 404 的经典事故)。

练习

  1. 跑 ls -l /,把里面所有软链接找出来并解释它们指向哪儿(提示:第一个字符是 l 的行)。再回答:这台机器上 /bin/ls 实际在哪个目录?
  2. 用 findmnt 分别确认 /、/tmp、/run、/mnt/c 的文件系统类型,并说明哪两个重启后会清空、哪一个是 Windows 的盘。
  3. cat /proc/filesystems 里找不到的格式(比如 btrfs),用 root 执行 modprobe btrfs 后再看一次,解释发生了什么。
  4. 造一个 5 行的文件,用 vim 依次完成:删第 2 行、复制第 1 行到末尾、撤销、反撤销、把第 1 到 3 行大写。每一步都用 :set number? 之外的方式验证结果(比如另开终端 cat)。
  5. 建 ~/.vimrc 并写入本文那 11 行,然后回答三个问题:为什么写 Python 时建议去掉 cindent?:set mouse=a 之后终端复制文字为什么会变奇怪?怎么确认某条设置最终是哪个文件生效的(提示 :verbose set xxx?)?
  6. 打开一个文件后 kill -9 掉终端,回来找那个隐藏交换文件,用 vim -r 列出它,再走一遍恢复流程,最后把它删掉。
  7. 分别用 --help、man、man -f、man -k、info 查同一个命令(建议 tar),把五种输出的差别写下来。
  8. 查 man 5 fstab 和 man 8 mount,然后解释:/etc/fstab 里一行由哪几列组成?为什么改它之前要备份?

参考