强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Unix 设计哲学教程 / 第 3 章:一切皆文件

第 3 章:一切皆文件

“In Unix, everything is a file.” — 这是 Unix 最优雅、最深刻的设计决策。

Unix 将系统中的几乎所有资源——普通文件、目录、硬盘、终端、网络连接、进程信息——都抽象为文件。这意味着你只需要一套 API(openreadwriteclose)就能操作所有资源。


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.xLinux 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"

注意事项

  1. /proc 和 /sys 是内存文件系统:重启后所有通过 echo 写入的修改都会丢失。需要持久化的配置应写入 /etc/sysctl.conf 或 udev 规则。
  2. 不要随意修改 /proc/sys:错误的内核参数可能导致系统不稳定或无法访问。
  3. /dev/urandom vs /dev/random:在 Linux 中,/dev/urandom 在大多数场景下是安全的。/dev/random 可能会阻塞,但对密码学场景有更好的理论安全性。
  4. 文件描述符泄漏:程序打开了文件描述符却没有关闭,是常见的资源泄漏问题。使用 lsof -p PID 检查进程打开的文件描述符数量。
  5. 容器环境中的 /proc:在 Docker 容器中,/proc 是受限的(部分信息被 namespace 隔离),某些文件可能无法读取。

扩展阅读