Skip to content

02|文件管理

第 1 讲登录机器后敲 ls /,看到根目录下一堆目录名。这一篇讲怎么操作这些目录和文件——怎么进目录、怎么找文件、怎么看文件内容、怎么复制移动删除、怎么改文件。

Linux 上一切皆文件——目录、设备、网络连接都以"文件"的形式存在。这个设计让操作方式统一:不管操作什么,基本都是同一套文件命令。本篇从文件类型讲起,依次介绍目录浏览、内容查看、增删改、查找、链接。

一、文件类型

先看一个文件长什么样。ls -l 列出文件详情,第一列首字符标记了文件类型:

bash
$ ls -l /etc/passwd /etc /dev/null
-rw-r--r--  1 root root  2841 Jun 23 10:22 /etc/passwd
drwxr-xr-x  1 root root  4096 Jun 22 18:00 /etc
crw-rw-rw-  1 root root  1, 3 Jun 23 09:10 /dev/null

首字符和文件类型的对应关系:

标记类型含义
-普通文件文本、二进制、压缩包等
d目录目录本身也是一种文件,内部记录文件名和 inode 的映射
l软链接指向另一个路径的"快捷方式"
c字符设备按字符流读写的设备(终端、串口)
b块设备按数据块读写的设备(硬盘、SSD)
ssocket进程间通信的套接字文件
p管道命名管道(FIFO),用于进程间数据传输

仅凭扩展名判断文件类型不可靠。Linux 不强制扩展名规则——一个 .log 文件可能是文本,也可能是 gzip 压缩后的旧日志,还可能是任意二进制。准确的做法是用 file 命令读取文件头部的 magic number 来判断真实类型:

bash
$ file /bin/ls
/bin/ls: ELF 64-bit LSB pie executable, x86-64, ...

$ file /etc/passwd
/etc/passwd: ASCII text

$ file app.log.1
app.log.1: gzip compressed data, ...

排查"日志打不开"或"文件内容显示乱码"时,优先用 file 确认实际类型。logrotate 轮转后的日志通常会被 gzip 压缩成 .log.1.gz.log.1(不带后缀但实际是压缩文件),此时必须用 zcatzlessgunzip 处理,直接 cat 会看到一堆乱码。

bash
zcat /var/log/nginx/access.log.1.gz | tail -20    # 查看压缩日志的最后 20 行
zless /var/log/nginx/access.log.1.gz              # 翻页查看压缩日志

二、目录浏览

ls 是最高频的命令之一,常用参数组合:

bash
ls -l      # 长格式,显示权限、属主、大小、时间
ls -la     # 加 -a 显示隐藏文件(以 . 开头)
ls -lh     # 大小以人类可读格式显示(K/M/G)
ls -ltr    # 按修改时间排序,最新在最后

ls -lhtr 在排查日志目录时非常实用——-t 按时间排序,-r 反序,使最新的文件出现在底部,无需向上翻页查找:

bash
$ ls -lhtr /var/log
... (旧日志在上)
-rw-r----- 1 syslog adm  12M Jun 23 09:55 syslog
-rw-r----- 1 syslog adm 2.3M Jun 23 10:23 auth.log

如果只关心最近 1 小时新生成或被修改的日志,可以配合 find 进一步过滤(详见第五节)。

系统目录布局

登录一台机器敲 ls /,会看到根目录下一堆目录名。这些是 Linux 文件系统的标准布局,每个目录放特定类型的东西。几个高频目录要先认识:

目录放什么
/etc系统和服务的配置文件(Nginx、SSH、MySQL 的配置都在这)
/var/log日志输出(磁盘最容易被打满的目录)
/var/lib服务运行时数据(数据库数据文件、容器存储)
/opt整包安装的第三方软件、自建服务部署
/usr/local手工编译安装的软件
/home普通用户家目录
/rootroot 用户家目录
/tmp临时文件(部分系统重启会清空,别放重要数据)
/proc内核与进程信息(虚拟文件系统,不占磁盘)
/sys设备与内核对象(虚拟,不占磁盘)
/dev设备文件(磁盘、终端等)

排查问题时这些目录的用途要心中有数。改配置去 /etc,看日志去 /var/log,找数据库数据去 /var/lib。找不到对应文件,往往是因为不知道该去哪个目录找。

/var/log 是磁盘最容易被打满的目录。业务跑一段时间日志持续累积,没配清理任务的话某天磁盘就满了。找占用最大的目录:

bash
du -sh /var/log/* 2>/dev/null | sort -hr | head -10

/tmp 有个坑要知道——部分发行版配置了重启后清空 /tmp 的策略,甚至把 /tmp 挂成基于内存的 tmpfs。需要长期保存的数据不能放 /tmp,写入 /tmp 的数据可能占内存。

/proc/sys 是内核实时生成的虚拟文件系统,不占磁盘。排查时常用的 /proc 接口:

bash
cat /proc/cpuinfo       # CPU 信息
cat /proc/meminfo       # 内存使用详情
cat /proc/<PID>/cmdline # 某进程的完整启动命令
cat /proc/<PID>/status  # 某进程的详细状态

这些信息执行 cat 时由内核实时返回,不是存盘的文件。

三、内容查看

1. 全文输出与翻页

小文件直接 cat 一次输出:

bash
cat /etc/hosts

大文件(几十 MB 以上)不要用 cat,会刷屏并占用大量终端缓冲区。改用 less,支持双向翻页、搜索、跳转:

bash
less /var/log/messages

less 的高频快捷键:

按键作用
/keyword向下搜索关键字
?keyword向上搜索关键字
n / N跳到下一个 / 上一个匹配
g / G跳到文件开头 / 末尾
q退出

2. 头尾与实时跟踪

只看文件开头或结尾的指定行数:

bash
head -n 20 app.log     # 前 20 行
tail -n 50 app.log     # 最后 50 行

实时跟踪文件新增内容,用 -f 参数:

bash
tail -f /var/log/nginx/access.log

tail -f 是排查问题时的核心工具,适用于流量适中的日志监控。如果日志写入速度极快(高 QPS 的访问日志),直接 tail -f 输出会无法肉眼跟读,需要配合 grep 过滤关键字:

bash
tail -f /var/log/nginx/access.log | grep " 500 "       # 实时过滤 500 错误
tail -f /var/log/nginx/access.log | grep -v " 200 "    # 实时过滤非 200 响应

tail -F(大写)比 -f 更健壮——当日志被 logrotate 轮转、原文件被删除时,-F 会自动重新打开新生成的同名文件,而 -f 会继续追踪已删除的旧文件描述符,看不到新日志。生产环境长时间观察建议使用 tail -F

四、复制、移动、删除

1. 复制:cp -a 优于 cp -r

bash
cp source.txt target.txt              # 复制单个文件
cp -r dir1 dir2                       # 递归复制目录
cp -a /etc/nginx /backup/nginx        # 归档模式复制,保留全部元数据

备份配置目录时应当使用 cp -a 而非 cp -r-a-dR --preserve=all 的等价写法,会同时保留属主、属组、权限模式、修改时间、软链接结构。cp -r 默认会丢失这些元数据——恢复配置时会出现"权限全乱、服务起不来"的状况。

对比两种方式的实际差异:

bash
# cp -r 后,新文件归属当前用户,权限按 umask 重新计算
$ cp -r /etc/ssh /tmp/ssh-backup
$ ls -l /tmp/ssh-backup/sshd_config
-rw-r--r-- 1 deploy deploy 3200 Jun 23 10:30 /tmp/ssh-backup/sshd_config

# cp -a 后,所有元数据完整保留
$ cp -a /etc/ssh /tmp/ssh-backup-a
$ ls -l /tmp/ssh-backup-a/sshd_config
-rw------- 1 root root 3200 Jun 22 18:00 /tmp/ssh-backup-a/sshd_config

2. 移动与重命名

mv 同时承担移动和重命名两种语义,具体取决于目标路径:

bash
mv old.txt new.txt                # 重命名(同一目录内)
mv /tmp/data.csv /var/backup/     # 移动到其他目录

跨文件系统移动时,mv 实际上执行的是"复制 + 删除原文件",过程不是原子的,如果中途被中断,可能出现源文件已经损坏、目标文件不完整的情况。跨盘移动大文件时,优先 cp -a + 验证 + rm 三步操作,而不是一条 mv

3. 删除:rm 的危险性

bash
rm file.txt          # 删除单个文件
rm -r dir            # 递归删除目录
rm -rf /path         # 强制递归删除,不询问确认

Linux 的 rm 没有回收站机制,删除后基本不可恢复(只能依赖文件系统层面的恢复工具,且成功率极低)。执行删除前,标准做法是先确认两件事:当前所在目录、目标路径:

bash
pwd
ls -ld /path/to/delete
rm -rf /path/to/delete

变量拼接路径的删除命令是事故高发场景。如果变量未赋值或为空,rm -rf /$UNDEFINED_VAR/ 会展开成 rm -rf //(等同于 rm -rf /),执行后整个系统的根目录被遍历删除。SteamOS 在 2015 年就因为类似的脚本 bug 删除过用户的全部数据。

防御方式:在脚本中执行任何变量拼接的 rm 之前,先 echo 验证展开结果:

bash
TARGET="/var/lib/app/$VERSION"
echo "即将删除:$TARGET"
[ -z "$VERSION" ] && { echo "VERSION 为空,中止"; exit 1; }
rm -rf "$TARGET"

或使用 set -u 让 shell 在变量未定义时直接报错退出,而不是静默展开成空字符串:

bash
#!/bin/bash
set -euo pipefail        # -u 让未定义变量直接报错
rm -rf "/var/lib/app/$VERSION"

五、文件查找

1. find 按条件搜索

find 支持按文件名、类型、大小、时间等条件搜索:

bash
find /etc -name "*.conf"              # 按文件名匹配
find /var -type f -size +100M         # 大于 100M 的普通文件
find /var/log -type f -mtime +7       # 修改时间在 7 天以前的文件
find /var/log -type f -mmin -30       # 30 分钟内修改过的文件
find / -type f -name "core.*" 2>/dev/null  # 全盘搜索 core dump 文件

关键参数说明:

参数含义
-name按文件名通配符匹配(区分大小写,-iname 不区分)
-type文件类型(f 普通文件、d 目录、l 软链接)
-size文件大小(+100M 大于 100M、-1k 小于 1k)
-mtime修改时间(以天为单位,+7 7 天前、-1 1 天内)
-mmin修改时间(以分钟为单位)
-user按属主过滤

2. 磁盘空间排查的固定套路

磁盘告警(/var 使用率 100% 之类)的排查流程几乎固定:先用 du 定位高占用目录,再用 find 找具体大文件:

bash
# 第 1 步:看根目录下各一级子目录的占用,定位故障目录
du -sh /* 2>/dev/null | sort -hr | head -10

# 第 2 步:逐层深入(假设定位到 /var)
du -sh /var/* 2>/dev/null | sort -hr | head -10

# 第 3 步:在定位到的目录里找出所有大于 1G 的文件
find /var -type f -size +1G -exec ls -lh {} \; 2>/dev/null

3. 已删除但未释放空间的文件

找到大文件后,清理前必须确认这个文件是否仍被进程持有。如果某进程仍持有该文件的文件描述符(例如 Nginx 仍在写 access.log),直接 rm 删除文件后,磁盘空间不会立即释放——文件名从目录树中消失了,但 inode 没有真正释放,因为进程引用计数未归零。

这种情况下,清理动作有两种:

  1. 重启占用文件的进程——释放文件描述符,空间立刻回收
  2. 使用 truncate -s 0 清空文件——保留 inode,直接将文件大小置零
bash
# 查看哪些被删除的文件仍被进程占用
lsof | grep deleted

# 不重启服务的清理方式:把日志文件清空
truncate -s 0 /var/log/nginx/access.log

> /var/log/nginx/access.log 这种重定向写法效果相同,但 truncate 语义更清晰。

六、软链接与硬链接

1. 软链接:文件版的快捷方式

软链接(符号链接)是一个特殊文件,内容是另一个文件的路径字符串。目标文件不存在时,软链接成为"断链",访问时报 No such file or directory

bash
ln -s /opt/app/releases/v1.2.3 /opt/app/current
ls -l /opt/app/current
# lrwxrwxrwx 1 deploy deploy 25 Jun 23 10:30 /opt/app/current -> /opt/app/releases/v1.2.3

软链接最典型的运维用法是版本切换。发布时让 /opt/app/current 软链接指向新版本目录,回滚时把链接指回旧版本目录,应用代码始终读取 /opt/app/current 这一固定路径,完全不感知背后的版本切换:

bash
# 发布 v1.2.4
ln -sfn /opt/app/releases/v1.2.4 /opt/app/current

# 回滚到 v1.2.3
ln -sfn /opt/app/releases/v1.2.3 /opt/app/current

ln 的三个参数含义:

参数作用
-s创建软链接(symbolic)
-f强制覆盖已有的同名链接
-n当目标是指向目录的链接时,替换链接本身,而不是钻进目录里创建链接

-n 是关键参数。如果省略 -n,当 /opt/app/current 本身已经是指向某目录的软链接时,ln -sf 会进入该目录,在内部创建一个新链接,而不是替换 current 本身——这会导致版本切换失败,且不容易立刻发现。

2. 硬链接:同一 inode 的多个名字

硬链接与目标文件指向同一个 inode(文件系统中存储元数据和数据块位置的结构)。同一个 inode 可以有多个文件名,删除其中任意一个,只要还有别的文件名指向该 inode,数据就完整保留。

bash
$ ln app.log app.log.bak
$ ls -li app.log app.log.bak
123456 -rw-r--r-- 2 deploy deploy 1024 Jun 23 10:30 app.log
123456 -rw-r--r-- 2 deploy deploy 1024 Jun 23 10:30 app.log.bak

注意第一列 inode 号(123456)完全相同,第三列的硬链接数变成了 2。

硬链接在日常运维中很少手动创建,但它背后的引用计数机制直接解释了一个常见故障:

rm 删除日志文件后,磁盘空间没释放——原因是该文件仍被某进程持有(进程内部维护一个文件描述符,等同于一个额外的"链接")。inode 的引用计数没有归零,文件系统不会真正回收空间。lsof | grep deleted 可以列出所有这种"已删除但仍被占用"的文件:

bash
$ lsof | grep deleted
nginx  12345  www-data  3w  REG  8,1  10737418240  789012  /var/log/nginx/access.log (deleted)

输出含义:nginx 进程(PID 12345)仍以写模式(3w)持有 /var/log/nginx/access.log 这个已被删除的文件,占用 10G 空间。修复方式与上一节相同:重启进程或使用 truncate 清空。

七、文件时间戳

每个文件实际上有三个时间,而不是直观以为的一个。stat 命令可以一次查看全部:

bash
$ stat /etc/hosts
  File: /etc/hosts
  Size: 220       Blocks: 8          IO Block: 4096   regular file
Device: 801h/2049d  Inode: 131073      Links: 1
Access: (0644/-rw-r--r--)  Uid: (0/root)   Gid: (0/root)
Access: 2026-06-23 10:30:15.123456789 +0800
Modify: 2026-06-22 18:00:42.987654321 +0800
Change: 2026-06-22 18:00:42.987654321 +0800
字段名称触发更新的操作
Accessatime读取文件内容(catless、程序读取)
Modifymtime修改文件内容
Changectime修改文件内容、权限、属主、链接等任何元数据

注意 mtime 和 ctime 的区别:chmod 只改权限,不改文件内容,但会更新 ctime,不会更新 mtime。这一区别在排查"谁动过这个文件"时非常关键——发现 mtime 没变但 ctime 变了,说明有人改了权限或属主,而没有修改内容。

排查"配置改了为什么没生效"时,statls -l 更有用——它能同时显示三个时间和 inode 号。运维侧经常遇到的情况是:修改的文件路径与服务实际加载的文件路径并不是同一个——可能是软链接指向了别处,或者同一文件名在多个路径下都存在。通过 inode 号比对,可以确认"修改的文件"和"服务加载的文件"是不是同一个:

bash
stat /etc/nginx/nginx.conf
stat /etc/alternatives/nginx-conf      # 假设这是个软链接

如果两条命令输出的 inode 号相同,说明是同一个文件;不同则说明修改了错误的文件。