3.1 文件系统:一切皆文件
【Linux 操作系统深度】系列 Part 1(免费)。 这一篇讲透 Linux 最根本的设计思想——"一切皆文件":为什么硬盘、键盘、进程都能像文件一样读写,FHS 标准目录各放什么,以及七种文件类型怎么辨认。 Part 2 讲 inode、软硬链接与挂载机制(星球专享),Part 3 讲用户组与 rwx 权限位,Part 4 讲管道重定向与 Shell 脚本。
一、一个问题:Linux 里"文件"到底指什么
刚从 Windows 过来的同学,脑子里"文件"的概念通常是:一个有扩展名、有图标、躺在文件夹里的东西。.txt 是文本,.exe 是程序,C 盘 D 盘是分开的磁盘。
Linux 完全不是这个模型。它有一句贯穿整个系统的设计哲学:
Everything is a file —— 一切皆文件。
这句话的意思是:内核对外部世界的统一抽象方式,就是"文件"。不管你操作的是:
- 一个存了数据的普通文本;
- 一个目录(在 Linux 里目录也是一种"文件");
- 一块硬盘、一个 U 盘;
- 键盘、终端、打印机;
- 一张网卡;
- 甚至一个正在运行的进程的信息;
内核都尽量用同一套接口来对待它们:open() 打开、read() 读、write() 写、close() 关闭。
这不是一句口号,它直接决定了你在 Linux 里的操作方式。下面先建立整体图景,再一层层拆开。
二、一张图看懂"一切皆文件"
中间这一层叫 VFS(Virtual File System,虚拟文件系统) ,它是内核里的一个抽象层。VFS 定义了"一个文件应该支持哪些操作",然后让不同的具体文件系统(ext4、procfs、设备驱动……)各自实现这些操作。
对你和上层程序来说,不需要知道背后是磁盘还是设备,统一调 read() 就行;VFS 会把请求转给真正干活的那一方。这就是"一切皆文件"在代码层面的实现。
一个直观的例子
你可以用同一条 cat 命令,读完全不同的东西:
cat /etc/hostname # 读一个普通文件 → 输出主机名
cat /dev/null # 读一个字符设备(黑洞)→ 什么都不输出
cat /proc/cpuinfo # 读内核虚拟文件 → 输出 CPU 信息
cat /dev/zero | head -c 16 # 读字符设备(零源)→ 输出 16 个字节的 0cat 程序内部只做了一件事:open() → 循环 read() → 把读到的内容写到屏幕。它根本不关心、也不需要知道自己读的是哪种"文件"。这种统一带来的简洁,就是"一切皆文件"的价值。
三、FHS:Linux 的目录地图
Linux 没有 C 盘 D 盘的概念,所有文件——不管在哪块磁盘上——都挂在同一棵从根 / 开始的目录树里。这棵树的顶层布局不是随便长的,它遵循一个叫 FHS(Filesystem Hierarchy Standard,文件系统层次标准) 的规范。
记住下面这张表,你就拿到了 Linux 系统的"地图":
| 目录 | 名字来源 | 放什么 | 安全/运维相关性 |
|---|---|---|---|
/ | root | 根目录,整棵树的起点 | — |
/bin | binaries | 所有用户都能用的基础命令(ls、cat、cp) | 应急时必须在 |
/sbin | system binaries | 需要 root 的系统管理命令(reboot、fdisk) | 提权、运维 |
/etc | et cetera | 系统和服务的配置文件(纯文本) | 改配置、找口令/密钥 |
/home | home | 普通用户的家目录,每人一个子目录 | 用户数据、.ssh 密钥 |
/root | root | root 用户的家目录(不在 /home 下) | 管理员痕迹 |
/usr | unix system resources | 大部分程序和数据(只读),下分 bin/lib/share | 安装的软件 |
/var | variable | 经常变化的数据:日志、缓存、邮件、数据库 | /var/log 看日志 |
/tmp | temporary | 临时文件,重启可能清空 | 临时 payload、提权 |
/dev | devices | 设备文件(磁盘、null、zero) | 一切皆文件的体现 |
/proc | processes | 进程与内核信息虚拟文件系统 | 看进程、环境变量 |
/sys | system | 硬件与内核参数虚拟文件系统 | 设备、驱动信息 |
/lib | library | /bin 和 /sbin 需要的共享库和内核模块 | — |
/opt | optional | 第三方/可选的附加软件 | 手动装的软件 |
/boot | boot | 启动引导程序和内核(vmlinuz、grub) | 启动相关 |
/mnt /media | mount | 临时挂载点(额外磁盘、U 盘、光盘) | 挂载新存储 |
几个安全学习中最常打交道的目录,单独强调:
/etc:服务配置都在这。比如/etc/passwd(用户列表)、/etc/shadow(密码哈希,root 才能读)、/etc/ssh/sshd_config(SSH 配置)。做安全时,配置文件里的弱口令、硬编码密钥、错误权限都是重点。/var/log:所有日志的家。/var/log/auth.log(登录记录,Debian 系)、/var/log/secure(Red Hat 系)是排查入侵和追踪行为的第一站。/proc:每个运行中的进程在/proc/<PID>/下有一个"目录",里面的cmdline(启动命令)、environ(环境变量,可能含口令)、fd/(打开的文件)都是信息收集的金矿。/tmp和/dev/shm:全局可写,临时文件和内存文件系统常被用来放临时工具;排查时值得留意。
小知识:现代很多发行版(如 Ubuntu)里,
/bin/sbin/lib其实是指向/usr/bin/usr/sbin/usr/lib的软链接(合并到 /usr 的方案,usrmerge),所以你ls -l /bin会看到它是个链接。这是历史演进,不影响使用。
四、七种文件类型:ls -l 第一列会说话
"一切皆文件"不等于所有文件长得一样。Linux 把文件分成七种类型,用 ls -l 输出的第一个字符就能辨认。
$ ls -l /etc/hostname
-rw-r--r-- 1 root root 12 9月 20 10:00 /etc/hostname
↑
第一个字符就是类型| 首字符 | 类型 | 说明 | 典型例子 |
|---|---|---|---|
- | 普通文件 regular | 数据的载体,文本或二进制 | /etc/hostname、脚本、ELF 程序 |
d | 目录 directory | 存放"文件名 → inode"映射的特殊文件 | /home、/tmp |
l | 符号链接 symbolic link | 指向另一个路径的快捷方式(软链接) | /etc/mtab、/bin→/usr/bin |
b | 块设备 block device | 按块随机存取,通常是磁盘/存储 | /dev/sda、/dev/nvme0n1 |
c | 字符设备 character device | 按字节流顺序读写 | /dev/null、/dev/tty、/dev/zero |
p | 命名管道 named pipe | 进程间传递数据的 FIFO | mkfifo 创建 |
s | 套接字 socket | 进程间/网络通信的端点 | /run/docker.sock、MySQL socket |
4.1 普通文件(-)
最常见的一类,里面是实际数据。按内容又分两种:
- 文本文件:人类可读的字符,配置、脚本、日志都是。
- 二进制文件:编译后的程序、图片、压缩包等,直接
cat会看到乱码。
关键区别:Linux 不靠扩展名判断文件类型。一个文件是文本还是程序,看的是内容本身。用 file 命令,它读文件开头的"魔数(magic number)"来判断:
$ file /bin/ls
/bin/ls: ELF 64-bit LSB pie executable, x86-64, ... # ELF 魔数 = 可执行程序
$ file /etc/hostname
/etc/hostname: ASCII text # 纯文本
$ file /dev/null
/dev/null: character special (1/3) # 字符设备
$ echo "hello" > fake.png
$ file fake.png
fake.png: ASCII text # 即使扩展名是 .png,内容是文本,file 照样认出来这和 Windows 靠扩展名关联程序完全不同。在 Linux 上,把脚本改名不影响它的本质;能不能"执行"取决于有没有执行权限 x,以及文件开头的 shebang(#!/bin/bash、#!/usr/bin/env python3)告诉内核用哪个解释器。
4.2 目录(d)
目录在磁盘上也是一个文件,只不过它的内容不是给人读的文章,而是一张**"文件名 → inode 编号"的对照表**。你 ls 一个目录,本质上是在读这张表;在目录里新建文件,就是往这张表加一条记录。
这也解释了为什么删除文件需要对所在目录有写权限(要改目录的对照表),而不是对文件本身有写权限。权限和 inode 的深入关系在 Part 2 讲。
4.3 符号链接(l)
软链接(symbolic link)是一个内容为"另一个路径"的特殊文件,类似 Windows 的快捷方式,但更彻底。访问软链接时,系统会自动跳到它指向的真实路径。
$ ls -l /usr/bin/python3
lrwxrwxrwx 1 root root 16 ... /usr/bin/python3 -> python3.10软链接可以跨文件系统、可以指向目录;如果目标被删除,链接就变成"悬空链接(dangling link)",显示为红色。与之相对的"硬链接"本质不同,Part 2 结合 inode 讲透二者区别。
4.4 设备文件(b / c)—— "一切皆文件"最典型的体现
设备文件位于 /dev,它们让你能用读写文件的方式操作硬件。分两类:
块设备(b):以固定大小的"块"为单位,支持随机寻址(跳到任意位置读写),主要是存储设备。
$ ls -l /dev/sda
brw-rw---- 1 root disk 8, 0 ... /dev/sda # 整块硬盘
$ ls -l /dev/sda1
brw-rw---- 1 root disk 8, 1 ... /dev/sda1 # 第一个分区字符设备(c):以字节流形式顺序读写,像水管一样,不能跳回开头。键盘、终端、以及几个非常常用的"虚拟设备"都是:
| 设备 | 行为 | 常见用法 |
|---|---|---|
/dev/null | 数据黑洞:写入的东西全部丢弃,读返回 EOF | command 2>/dev/null 丢弃错误输出 |
/dev/zero | 源源不断提供 \0 字节 | 生成指定大小文件、清空磁盘 |
/dev/random /dev/urandom | 提供随机字节 | 生成密钥、随机数 |
/dev/tty | 当前终端 | 脚本强制向终端输出 |
体会一下这套抽象:想丢弃输出,就"把它写进一个叫 null 的文件";想生成 1GB 的全零文件,就"从零源这个文件读 1GB 再写进新文件":
dd if=/dev/zero of=disk.img bs=1M count=1024 # 生成 1GB 全零镜像
head -c 32 /dev/urandom | xxd # 生成 32 字节随机数(可当密钥)4.5 管道(p)和套接字(s)
这两类用于进程之间通信,同样被抽象成文件:
- 命名管道(p,FIFO):一个进程往里写,另一个进程从里读,数据像水流过管子。用
mkfifo mypipe创建。命令行里的匿名管道|是同一思想的临时形态(Part 4 详解)。 - 套接字(s):网络通信和本机进程间通信的端点。做安全最常遇到的是
/run/docker.sock——Docker 守护进程的 Unix 套接字。谁能读写这个套接字,谁就能控制 Docker(进而等价于 root,容器逃逸的经典路径);MySQL/Nginx 也可能用本机套接字通信。
五、/proc 与 /sys:不存在于磁盘的"文件"
除了设备文件,"一切皆文件"还有一个更神奇的延伸:内核把自己的内部状态也做成了文件。
5.1 /proc:进程与内核的窗口
/proc 是 procfs 虚拟文件系统挂载点。这里的文件不占磁盘,在你读取的瞬间由内核动态生成。
$ ls /proc
1 10 cpuinfo meminfo version ...
└┬─┘
每个数字 = 一个进程的 PID,对应一个"目录"常用的信息文件:
| 路径 | 内容 |
|---|---|
/proc/cpuinfo | CPU 型号、核心数、特性 |
/proc/meminfo | 内存使用详情(free 命令的数据来源) |
/proc/version | 内核版本 |
/proc/mounts | 当前挂载的文件系统 |
/proc/net/tcp | TCP 连接状态(网络排查) |
/proc/sys/... | 可调整的内核参数(sysctl) |
每个进程的 /proc/<PID>/ 目录是信息收集重点:
| 路径 | 内容 | 安全相关性 |
|---|---|---|
/proc/<PID>/cmdline | 进程启动命令(含参数) | 还原服务怎么启动的 |
/proc/<PID>/environ | 环境变量 | 可能含密码、密钥、令牌 |
/proc/<PID>/fd/ | 该进程打开的所有文件 | 找它在读什么、日志、socket |
/proc/<PID>/maps | 内存映射 | 分析二进制、找 libc 基址 |
/proc/<PID>/status | 运行状态、权限、UID/GID | 看进程以谁的身份跑 |
# 例:查看 1 号进程(systemd)的启动命令
cat /proc/1/cmdline | tr '\0' ' '注意 cmdline 和 environ 里各项用
\0(空字节)分隔,直接 cat 会粘在一起,管道加tr '\0' '\n'或' '转换后更易读。
5.2 /sys:硬件与设备的统一模型
/sys 是 sysfs,比 /proc 更规整,按统一模型暴露硬件设备、驱动、内核子系统的信息。比如 /sys/class/net/ 下是网卡,/sys/block/ 下是块设备。日常排查硬件、设备识别时会用到;安全场景相对 /proc 用得少,知道它也是"内核状态文件化"即可。
5.3 为什么这套设计如此重要
把进程信息、硬件、内核参数都做成文件,带来的直接好处是:你不需要为每种信息学一套专用工具。
- 想看 CPU?
cat /proc/cpuinfo; - 想知道某进程的环境变量?
cat /proc/1234/environ; - 想改内核参数(如开启 IP 转发)?往
/proc/sys/net/ipv4/ip_forward写个1:echo 1 > /proc/sys/net/ipv4/ip_forward。
"读文件 = 查状态,写文件 = 改配置",一套读写语义通吃。这正是 Unix/Linux "机制统一、组合简单"的工程美学,也是后面学命令注入、提权、容器逃逸时反复用到的底层认知。
六、动手:在系统里辨认这些概念
打开任意一台 Linux(云服务器、WSL、或靶场机),按下面顺序敲一遍,比只读文章有效得多:
# 1. 看整棵目录树的顶层布局,对照 FHS 表
ls -l /
# 2. 辨认七种文件类型(注意第一列字符)
ls -l /etc/hostname # - 普通文件
ls -ld /etc # d 目录
ls -l /bin # l 软链接(usrmerge 系统)
ls -l /dev/sda # b 块设备(没有磁盘就看 /dev/loop0)
ls -l /dev/null # c 字符设备
ls -l /run/docker.sock # s 套接字(装了 docker 才有)
# 3. 用 file 看真实类型,验证"不靠扩展名"
file /bin/ls /etc/hostname /dev/null
echo "x" > /tmp/test.png && file /tmp/test.png # 扩展名骗人
# 4. 体验 /proc 虚拟文件
cat /proc/cpuinfo | head
cat /proc/meminfo | head
ls /proc/self/ # self = 当前正在运行的命令自己
cat /proc/self/cmdline | tr '\0' ' '; echo
# 5. 体验设备文件
dd if=/dev/zero of=/tmp/zeros.bin bs=1M count=1 # 从零源造 1MB 文件
ls -lh /tmp/zeros.bin
command 2>/dev/null; echo "错误已被 /dev/null 吞掉"做完这一遍,你应该能:对着 ls -l 输出脱口说出文件类型、解释 /dev 和 /proc 为什么也是"文件"、以及为什么改扩展名骗不了 Linux。
七、本篇小结
- "一切皆文件"是 Linux 的统一抽象:普通数据、目录、设备、进程状态、通信端点,内核都通过 VFS 用 open/read/write 一套接口对待;具体差异由各文件系统和驱动实现。
- FHS 规定了目录地图:配置在
/etc、日志在/var/log、用户在/home、设备在/dev、进程信息在/proc、程序大多在/usr。 - 七种文件类型看
ls -l首字符:-普通、d目录、l软链接、b块设备、c字符设备、p管道、s套接字。 - Linux 不靠扩展名判断类型,
file命令读文件头魔数;能否执行看 x 权限和 shebang/ELF 头。 - /proc 和 /sys 是内核实时生成的虚拟文件,读它就是在查内核状态,写它就是在改配置,不占磁盘。
理解了"文件"这个统一抽象,下一篇我们钻进文件系统的底层:一个文件在磁盘上到底是怎么存的、inode 是什么、硬链接和软链接有什么本质区别、一块新磁盘又是怎么"挂"进这棵目录树的。