Unix 设计哲学教程 / 第 3 章:一切皆文件
第 3 章:一切皆文件
“In Unix, everything is a file.” — 这是 Unix 最优雅、最深刻的设计决策。
Unix 将系统中的几乎所有资源——普通文件、目录、硬盘、终端、网络连接、进程信息——都抽象为文件。这意味着你只需要一套 API(open、read、write、close)就能操作所有资源。
3.1 统一接口的威力
一切皆文件的含义
Unix 中 "文件" 的范围
├── 普通文件 —— /home/user/document.txt
├── 目录 —— /home/user/(特殊格式的文件)
├── 块设备 —— /dev/sda(硬盘)
├── 字符设备 —— /dev/tty(终端)
├── 符号链接 —— /usr/bin/python → python3
├── 命名管道 —— /tmp/myfifo(FIFO)
├── 套接字 —— /var/run/docker.sock(Unix Domain Socket)
├── 进程信息 —— /proc/1/cmdline
├── 内核参数 —— /proc/sys/net/ipv4/ip_forward
└── 硬件信息 —— /sys/class/net/eth0/address
统一接口的好处
/* 所有 I/O 操作都使用相同的系统调用 */
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
/* 读普通文件 */
fd = open("/etc/hostname", O_RDONLY);
/* 读设备文件 —— 同样的 open/read/close */
fd = open("/dev/urandom", O_RDONLY);
/* 读进程信息 —— 同样的 open/read/close */
fd = open("/proc/cpuinfo", O_RDONLY);
/* 写入终端 —— 同样的 open/write/close */
fd = open("/dev/tty", O_WRONLY);
/* 所有操作都使用 read(fd, buf, count) */
char buf[256];
ssize_t n = read(fd, buf, sizeof(buf));
close(fd);
return 0;
}
3.2 文件描述符(File Descriptor)
三个标准文件描述符
每个进程启动时自动获得三个文件描述符:
| 文件描述符 | 名称 | 宏定义 | 默认连接 |
|---|---|---|---|
| 0 | 标准输入(stdin) | STDIN_FILENO | 键盘 |
| 1 | 标准输出(stdout) | STDOUT_FILENO | 终端 |
| 2 | 标准错误(stderr) | STDERR_FILENO | 终端 |
# 查看当前 Shell 进程的文件描述符
ls -la /proc/$$/fd/
# 或
ls -la /dev/fd/
# 典型输出:
# lrwx------ 1 user user 64 May 10 10:00 0 -> /dev/pts/0
# lrwx------ 1 user user 64 May 10 10:00 1 -> /dev/pts/0
# lrwx------ 1 user user 64 May 10 10:00 2 -> /dev/pts/0
文件描述符的操作
/* 文件描述符的底层操作 */
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main() {
/* open() —— 打开文件,返回文件描述符 */
int fd = open("/tmp/test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open"); // 输出到 stderr
return 1;
}
/* write() —— 写入数据 */
const char *msg = "Hello, Unix!\n";
ssize_t written = write(fd, msg, strlen(msg));
/* lseek() —— 移动文件指针 */
off_t pos = lseek(fd, 0, SEEK_SET); // 回到文件开头
/* dup2() —— 复制文件描述符 */
int fd_copy = dup(fd); // 复制 fd 到新的描述符
dup2(fd, STDOUT_FILENO); // 将 stdout 重定向到文件
/* close() —— 关闭文件描述符 */
close(fd);
return 0;
}
文件描述符的继承
# 子进程继承父进程的文件描述符
# 这是管道工作的基础
# 查看进程的文件描述符限制
ulimit -n
# 通常默认 1024 或 65536
# 查看系统级限制
cat /proc/sys/fs/file-max
# 可能是几百万
# 修改当前 Shell 的限制
ulimit -n 65536
3.3 设备文件(Device Files)
块设备与字符设备
# 查看设备文件
ls -la /dev/sda
# brw-rw---- 1 root disk 8, 0 May 10 10:00 /dev/sda
# ^b 表示块设备(block device)
ls -la /dev/tty
# crw-rw-rw- 1 root tty 5, 0 May 10 10:00 /dev/tty
# ^c 表示字符设备(character device)
| 设备类型 | 特征 | 典型设备 | 用途 |
|---|---|---|---|
| 块设备 | 按块(通常 512B/4KB)随机访问 | /dev/sda, /dev/nvme0n1 | 磁盘、SSD |
| 字符设备 | 按字符流式访问 | /dev/tty, /dev/null | 终端、串口 |
重要的设备文件
# /dev/null —— 黑洞:写入的数据被丢弃,读取立即返回 EOF
echo "这条消息消失了" > /dev/null
cat /dev/null > file.txt # 清空文件
# /dev/zero —— 零源:读取无限个 \0 字节
dd if=/dev/zero of=zeros.bin bs=1M count=10 # 创建 10MB 零字节文件
# /dev/urandom —— 随机数生成器
head -c 32 /dev/urandom | base64 # 生成 32 字节随机密钥
# /dev/full —— 总是返回 "磁盘已满" 错误
echo "test" > /dev/full
# bash: echo: write error: No space left on device
# /dev/stdin, /dev/stdout, /dev/stderr —— 标准流的文件表示
echo "hello" > /dev/stderr # 输出到标准错误
设备文件的创建
# 创建字符设备(需要 root)
# mknod 设备文件 类型 主设备号 次设备号
sudo mknod /dev/mydevice c 240 0
# 创建命名管道(FIFO)
mkfifo /tmp/mypipe
echo "hello" > /tmp/mypipe & # 写入端(后台)
cat /tmp/mypipe # 读取端
# 输出: hello
3.4 /proc 文件系统
进程信息
/proc 是一个虚拟文件系统(Virtual File System),它不占用磁盘空间,而是由内核在运行时动态生成。
# 查看当前所有进程
ls /proc/
# 1 2 3 42 100 ... self
# 查看 PID 1(init/systemd)的信息
cat /proc/1/cmdline # 启动命令
cat /proc/1/status # 进程状态(内存、线程数等)
cat /proc/1/environ # 环境变量
ls -la /proc/1/fd/ # 打开的文件描述符
cat /proc/1/maps # 内存映射
cat /proc/1/io # I/O 统计
# /proc/self 指向当前进程自身
cat /proc/self/cmdline # 输出 cat 自己的命令行
系统信息
# CPU 信息
cat /proc/cpuinfo
# 内存信息
cat /proc/meminfo
# MemTotal: 16384000 kB
# MemFree: 8192000 kB
# MemAvailable: 12288000 kB
# Buffers: 512000 kB
# Cached: 3072000 kB
# 内核版本
cat /proc/version
# 已挂载的文件系统
cat /proc/mounts
# 网络统计
cat /proc/net/dev # 网卡流量
cat /proc/net/tcp # TCP 连接
cat /proc/net/arp # ARP 表
# 内核模块
cat /proc/modules
# 系统运行时间
cat /proc/uptime
# 86400.50 72000.30 (总时间 空闲时间)
/proc/sys —— 内核参数
# /proc/sys 可以读取和修改内核参数
# 这是"一切皆文件"哲学的极致体现——连内核配置都是文件!
# 查看 IP 转发是否开启
cat /proc/sys/net/ipv4/ip_forward
# 0 = 关闭, 1 = 开启
# 动态开启 IP 转发
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
# 查看主机名
cat /proc/sys/kernel/hostname
# 查看系统最大 PID
cat /proc/sys/kernel/pid_max
# 查看 TCP keepalive 时间
cat /proc/sys/net/ipv4/tcp_keepalive_time
# 查看和修改文件描述符限制
cat /proc/sys/fs/file-max
echo 2097152 | sudo tee /proc/sys/fs/file-max
# 查看 swappiness
cat /proc/sys/vm/swappiness
echo 10 | sudo tee /proc/sys/vm/swappiness
3.5 /sys 文件系统
/sys 与 /proc 的区别
| 特性 | /proc | /sys |
|---|---|---|
| 引入版本 | Linux 1.x | Linux 2.6 |
| 组织方式 | 按进程(/proc/PID/) | 按设备类别 |
| 主要用途 | 进程信息、内核参数 | 硬件设备、驱动信息 |
| 设计理念 | “信息堆场” | 结构化的设备模型 |
/sys 的结构
# /sys 的主要目录
ls /sys/
# block bus class dev devices firmware fs kernel module power
# 查看网络接口信息
ls /sys/class/net/
# eth0 lo wlan0
cat /sys/class/net/eth0/address # MAC 地址
cat /sys/class/net/eth0/mtu # MTU
cat /sys/class/net/eth0/operstate # 接口状态(up/down)
cat /sys/class/net/eth0/speed # 速率(Mbps)
# 查看磁盘信息
ls /sys/block/
# sda nvme0n1
cat /sys/block/sda/queue/rotational # 是否旋转盘(0=SSD, 1=HDD)
cat /sys/block/sda/queue/nr_requests # 队列深度
# 查看 CPU 信息
ls /sys/devices/system/cpu/
# cpu0 cpu1 cpu2 ... cpufreq ...
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq # 当前频率
# 查看温度传感器
ls /sys/class/thermal/
# thermal_zone0 thermal_zone1 ...
cat /sys/class/thermal/thermal_zone0/temp # 温度(毫摄氏度)
使用 /sys 控制 LED
# 嵌入式 Linux 中常见的用例:通过 /sys 控制 LED
ls /sys/class/leds/
# input0::capslock input0::numlock input0::scrolllock
# 关闭 Caps Lock LED
echo 0 | sudo tee /sys/class/leds/input0::capslock/brightness
# 设置 LED 触发器(如心跳闪烁)
echo heartbeat | sudo tee /sys/class/leds/input0::capslock/trigger
3.6 目录也是文件
目录的内部结构
在 Unix 中,目录是一种特殊格式的文件,包含文件名到 inode 号的映射表。
# 查看目录的内容(底层视角)
ls -lia /tmp/test/
# inode 号 权限 所有者 大小 文件名
# 1234567 -rw-r--r-- 1 user user 0 May 10 10:00 file1.txt
# 1234568 -rw-r--r-- 1 user user 0 May 10 10:00 file2.txt
# 查看 inode 信息
stat /tmp/test/file1.txt
# File: /tmp/test/file1.txt
# Size: 0 Blocks: 0 IO Block: 4096 regular empty file
# Inode: 1234567 Links: 1
# 查看文件系统 inode 使用情况
df -i
# Filesystem Inodes IUsed IFree IUse% Mounted on
# /dev/sda1 6553600 234567 6319033 4% /
inode 的作用
Unix 文件系统的三层结构
├── 文件名 → inode 号(目录文件中存储)
├── inode → 文件元数据(权限、所有者、大小、时间戳、数据块指针)
└── 数据块 → 文件的实际内容
这种分离使得:
├── 同一个文件可以有多个名字(硬链接)
├── 文件名的修改不影响文件数据
└── 移动文件(rename)不需要复制数据
# 硬链接:多个文件名指向同一个 inode
ln file.txt hardlink.txt
ls -li file.txt hardlink.txt
# 1234567 -rw-r--r-- 2 user user 1024 May 10 10:00 file.txt
# 1234567 -rw-r--r-- 2 user user 1024 May 10 10:00 hardlink.txt
# ↑ 同一个 inode,链接计数 = 2
# 符号链接:一个特殊文件,包含目标路径
ln -s file.txt symlink.txt
ls -la symlink.txt
# lrwxrwxrwx 1 user user 8 May 10 10:00 symlink.txt -> file.txt
3.7 命名管道(FIFO)
创建和使用
# 创建命名管道
mkfifo /tmp/mypipe
# 验证文件类型
ls -la /tmp/mypipe
# prw-r--r-- 1 user user 0 May 10 10:00 /tmp/mypipe
# ↑ p 表示管道(pipe)
# 使用场景 1:进程间通信
# 终端 1(读取端)
cat /tmp/mypipe
# 终端 2(写入端)
echo "Hello from process 2" > /tmp/mypipe
# 使用场景 2:数据中转
# 将 tar 输出直接传给另一个进程
mkfifo /tmp/tarpipe
tar czf /tmp/tarpipe /home/user/data &
scp /tmp/tarpipe remote:/backup/data.tar.gz
匿名管道 vs 命名管道
| 特性 | 匿名管道(|) | 命名管道(FIFO) |
|——|—————–|—————–|
| 生命周期 | 仅在命令执行期间存在 | 持久存在于文件系统中 |
| 使用方式 | cmd1 \| cmd2 | mkfifo pipe; cmd1 > pipe &; cmd2 < pipe |
| 关系限制 | 仅限父子进程 | 任意进程间 |
| 文件系统 | 无对应文件 | 有文件路径 |
3.8 Unix Domain Socket
Socket 也是文件
# Docker 守护进程通过 Unix Socket 通信
ls -la /var/run/docker.sock
# srw-rw---- 1 root docker 0 May 10 10:00 /var/run/docker.sock
# ↑ s 表示 socket
# 通过 curl 访问 Docker API
curl --unix-socket /var/run/docker.sock http://localhost/version
# systemd 的 socket 也是文件
ls -la /run/systemd/private
# srw-rw-rw- 1 root root 0 May 10 10:00 /run/systemd/private
通过 Socket 通信的示例
# Python: Unix Domain Socket 服务器
import socket
import os
SOCKET_PATH = "/tmp/echo.sock"
# 清理旧的 socket 文件
if os.path.exists(SOCKET_PATH):
os.unlink(SOCKET_PATH)
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(SOCKET_PATH)
server.listen(1)
print(f"Listening on {SOCKET_PATH}")
conn, _ = server.accept()
data = conn.recv(1024)
conn.sendall(data) # Echo 回去
conn.close()
# 用 socat 测试 Unix Socket
socat - UNIX-CONNECT:/tmp/echo.sock
# 输入消息,回车后会收到相同的回应
3.9 实战:利用"一切皆文件"解决问题
案例 1:监控磁盘 I/O
# 直接读取 /proc 中的磁盘统计
cat /proc/diskstats
# 8 0 sda 12345 0 567890 1234 0 0 0 0 0 0 0
# 实时监控磁盘 I/O
watch -n 1 'cat /proc/diskstats | grep sda'
# 使用 iostat(更友好的格式)
iostat -dx 1
案例 2:动态调整内核参数
# 场景:高并发 Web 服务器优化
#!/bin/bash
# 优化网络参数
echo 1 | sudo tee /proc/sys/net/ipv4/tcp_tw_reuse
echo 65535 | sudo tee /proc/sys/net/core/somaxconn
echo 1 | sudo tee /proc/sys/net/ipv4/tcp_fastopen
echo 65535 | sudo tee /proc/sys/net/ipv4/ip_local_port_range # 需要写入两个值
# 持久化配置(重启后生效)
cat >> /etc/sysctl.conf << 'EOF'
net.ipv4.tcp_tw_reuse = 1
net.core.somaxconn = 65535
net.ipv4.tcp_fastopen = 1
net.ipv4.ip_local_port_range = 1024 65535
fs.file-max = 2097152
vm.swappiness = 10
EOF
sudo sysctl -p # 重新加载配置
案例 3:进程间通信
# 使用命名管道实现简单的生产者-消费者模式
#!/bin/bash
FIFO="/tmp/work_queue"
mkfifo "$FIFO" 2>/dev/null
# 生产者
producer() {
for i in $(seq 1 10); do
echo "task_$i"
sleep 0.5
done
echo "DONE" # 终止信号
}
# 消费者
consumer() {
while IFS= read -r task; do
[ "$task" = "DONE" ] && break
echo "Processing: $task (PID $$)"
sleep 1
done
}
# 启动
producer > "$FIFO" &
consumer < "$FIFO"
rm "$FIFO"
注意事项
- /proc 和 /sys 是内存文件系统:重启后所有通过 echo 写入的修改都会丢失。需要持久化的配置应写入
/etc/sysctl.conf或 udev 规则。 - 不要随意修改 /proc/sys:错误的内核参数可能导致系统不稳定或无法访问。
- /dev/urandom vs /dev/random:在 Linux 中,
/dev/urandom在大多数场景下是安全的。/dev/random可能会阻塞,但对密码学场景有更好的理论安全性。 - 文件描述符泄漏:程序打开了文件描述符却没有关闭,是常见的资源泄漏问题。使用
lsof -p PID检查进程打开的文件描述符数量。 - 容器环境中的 /proc:在 Docker 容器中,/proc 是受限的(部分信息被 namespace 隔离),某些文件可能无法读取。
扩展阅读
- proc(5) man page — /proc 文件系统完整文档
- sysfs(5) man page — /sys 文件系统文档
- The /proc Filesystem (kernel.org)
- Linux Device Drivers — 设备文件的深层原理
- Understanding the Linux Virtual File System