文件系统
Linux操作系统秉承“一切皆文件”(Everything is a file)的设计哲学,这意味着系统中的资源(如磁盘、设备、网络连接等)都被抽象为文件接口,用户和程序可以通过统一的系统调用(如open()、read()、write()、close())来访问它们。这种抽象是通过虚拟文件系统(VFS,Virtual File System)实现的,VFS充当一个中间层,将不同的底层文件系统(如ext4、NTFS)和设备驱动统一起来。每个“文件”在内核中都对应一个inode(索引节点),inode存储元数据(如权限、所有者、时间戳),并指向实际数据块或设备处理函数。
在Linux中,文件类型不是基于扩展名,而是通过inode的模式(mode)位来区分。具体来说,ls -l命令的第一个字符就表示文件类型(从stat()系统调用获取)。
VFS
VFS 存在的根本目的
让用户态程序用统一的 POSIX 接口(open/read/write/close/stat 等)访问所有类型的“文件系统”,不管底层是 ext4、btrfs、xfs、nfs、tmpfs、procfs、sysfs、overlayfs、f2fs 还是各种奇奇怪怪的虚拟/伪文件系统。
VFS 的四大切入点对象(最核心的四个结构体)
现代 Linux 内核(5.x ~ 6.x)里,VFS 主要靠下面四个对象串起整个体系:
| 对象 | 内核结构体 | 对应现实概念 | 存放在哪里 | 主要职责 | 生命周期 |
|---|---|---|---|---|---|
| super_block | struct super_block | 文件系统实例(一个挂载点) | 内存 | 描述整个文件系统(块大小、魔数、根inode…) | mount → umount |
| inode | struct inode | 一个具体的文件/目录/设备/管道… | 内存 + 磁盘(大部分fs) | 元数据(权限、时间、链接数、大小、数据块指针…) | 文件存在期间(可被缓存) |
| dentry | struct dentry | 路径中的一个目录项(组件) | 内存(Dcache) | 路径解析、缓存文件名 → inode 的映射 | 只要路径被访问就有可能被缓存 |
| file | struct file | 进程打开的一个文件描述符 | 进程的 files_struct | 记录打开状态(偏移量、打开标志、读写位置…) | open → close(或进程退出) |
这四个对象的关系可以用一句话概括:
super_block → inode → dentry → file (一个文件系统 → 一个具体文件 → 路径名片段 → 某个进程打开的实例)
VFS 层典型的数据流(以 open + read + write 为例)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
用户态: open("/home/test.txt", O_RDWR)
↓
VFS层: sys_open → do_sys_open → do_filp_open
→ path_lookupat → link_path_walk(路径拆分走 Dcache / 真实查找)
→ dentry → inode 查找或创建
→ 调用具体文件系统的 inode->i_op->lookup() / .permission() 等
→ 分配 struct file → 填入 f_op(具体fs的 file_operations)
↓
用户态拿到 fd(文件描述符)
↓
用户态: read(fd, buf, 1024)
↓
VFS层: sys_read → fd → file → file->f_op->read() 或 ->read_iter()
→ 调用具体文件系统(ext4_read_iter / btrfs_read 等)
→ 页缓存 → 如果 miss 则真正读磁盘 / 网络
VFS 设计思想
-
一切皆文件(对象)思想 普通文件、目录、设备(/dev/sda)、socket、pipe、eventfd、inotify、bpf地图、cgroup、tracefs、debugfs…… 全都用 inode + dentry 表示,都能走同样的 open/read/write 接口。
-
Dentry cache + inode cache 路径查找极快(99%+ 命中 Dcache)。 inode 也有 slab 缓存,频繁访问的文件元数据基本不落盘。
-
file_operations / inode_operations / super_operations 每个文件系统只要实现这三组操作函数指针,就能接入 VFS。 这是一种非常经典的“面向对象 in C”的实现方式。
1 2 3 4 5 6 7 8 9
// 典型 ext4 的 file_operations 片段 const struct file_operations ext4_file_operations = { .read_iter = ext4_file_read_iter, .write_iter = ext4_file_write_iter, .mmap = ext4_file_mmap, .fsync = ext4_sync_file, .splice_read = ext4_file_splice_read, ... };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
// 文件名: tinyfs.c // 编译方式示例: make -C /lib/modules/$(uname -r)/build M=$(pwd) modules #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/pagemap.h> #include <linux/slab.h> #include <linux/buffer_head.h> #include <linux/mount.h> #include <linux/namei.h> #define TINYFS_MAGIC 0x20250609 #define TINYFS_ROOT_INO 1 // 前向声明 static struct inode_operations tinyfs_dir_inode_ops; static struct inode_operations tinyfs_file_inode_ops; static struct file_operations tinyfs_dir_fops; static struct file_operations tinyfs_file_fops; /* ====================== inode 操作 ====================== */ static struct inode *tinyfs_iget(struct super_block *sb, unsigned long ino) { struct inode *inode; inode = iget_locked(sb, ino); if (!inode) return ERR_PTR(-ENOMEM); if (!(inode->i_state & I_NEW)) return inode; inode->i_ino = ino; if (ino == TINYFS_ROOT_INO) { // 根目录 inode->i_mode = S_IFDIR | 0755; inode->i_uid = GLOBAL_ROOT_UID; inode->i_gid = GLOBAL_ROOT_GID; inode->i_size = 4096; inode->i_blocks = 0; inode->i_atime = inode->i_mtime = inode->i_ctime = current_time(inode); inode->i_op = &tinyfs_dir_inode_ops; inode->i_fop = &tinyfs_dir_fops; set_nlink(inode, 2); } else { // 普通文件(目前全部当普通文件处理) inode->i_mode = S_IFREG | 0644; inode->i_uid = GLOBAL_ROOT_UID; inode->i_gid = GLOBAL_ROOT_GID; inode->i_size = 0; inode->i_blocks = 0; inode->i_atime = inode->i_mtime = inode->i_ctime = current_time(inode); inode->i_op = &tinyfs_file_inode_ops; inode->i_fop = &tinyfs_file_fops; set_nlink(inode, 1); } unlock_new_inode(inode); return inode; } static struct inode *tinyfs_new_inode(struct inode *dir, umode_t mode) { struct inode *inode; ino_t ino; // 简单起见:我们用一个自增的假 inode 号(实际生产中要用位图或空闲链表) static atomic_t next_ino = ATOMIC_INIT(TINYFS_ROOT_INO + 1); ino = atomic_inc_return(&next_ino); inode = tinyfs_iget(dir->i_sb, ino); if (IS_ERR(inode)) return inode; inode_init_owner(inode, dir, mode); inode->i_size = 0; inode->i_blocks = 0; inode->i_atime = inode->i_mtime = inode->i_ctime = current_time(inode); return inode; } /* ====================== 目录操作 ====================== */ static int tinyfs_readdir(struct file *file, struct dir_context *ctx) { struct inode *inode = file_inode(file); if (!dir_emit_dots(file, ctx)) return 0; // 目前我们不维护目录项,所以只显示 . 和 .. return 0; } static int tinyfs_create(struct mnt_idmap *idmap, struct inode *dir, struct dentry *dentry, umode_t mode, bool excl) { struct inode *inode; inode = tinyfs_new_inode(dir, mode | S_IFREG); if (IS_ERR(inode)) return PTR_ERR(inode); d_instantiate(dentry, inode); dget(dentry); // 目录项引用计数 +1 dir->i_mtime = dir->i_ctime = current_time(dir); return 0; } static const struct inode_operations tinyfs_dir_inode_ops = { .create = tinyfs_create, // .lookup = simple_lookup, // 如果不想自己实现可以用这个 // .unlink = simple_unlink, // .mkdir = simple_mkdir, // .rmdir = simple_rmdir, }; static const struct file_operations tinyfs_dir_fops = { .llseek = generic_file_llseek, .read = generic_read_dir, .iterate_shared = tinyfs_readdir, }; /* ====================== 文件操作 ====================== */ static ssize_t tinyfs_read(struct file *file, char __user *buf, size_t len, loff_t *ppos) { // 目前所有文件内容都是空的 return 0; } static ssize_t tinyfs_write(struct file *file, const char __user *buf, size_t len, loff_t *ppos) { // 接受写入,但不真正保存(丢弃) struct inode *inode = file_inode(file); loff_t pos = *ppos; if (pos + len > inode->i_size) inode->i_size = pos + len; inode->i_mtime = inode->i_ctime = current_time(inode); *ppos = pos + len; return len; // 假装写成功了 } static const struct inode_operations tinyfs_file_inode_ops = { .setattr = simple_setattr, }; static const struct file_operations tinyfs_file_fops = { .read = tinyfs_read, .write = tinyfs_write, .llseek = generic_file_llseek, }; /* ====================== superblock 操作 ====================== */ static const struct super_operations tinyfs_sops = { .statfs = simple_statfs, .drop_inode = generic_delete_inode, }; static int tinyfs_fill_super(struct super_block *sb, struct fs_context *fc) { struct inode *root_inode; sb->s_maxbytes = 0; // 目前不限制 sb->s_blocksize = PAGE_SIZE; sb->s_blocksize_bits= PAGE_SHIFT; sb->s_magic = TINYFS_MAGIC; sb->s_op = &tinyfs_sops; sb->s_time_gran = 1; root_inode = tinyfs_iget(sb, TINYFS_ROOT_INO); if (IS_ERR(root_inode)) return PTR_ERR(root_inode); sb->s_root = d_make_root(root_inode); if (!sb->s_root) return -ENOMEM; return 0; } static int tinyfs_get_tree(struct fs_context *fc) { return get_tree_single(fc, tinyfs_fill_super); } static const struct fs_context_operations tinyfs_context_ops = { .get_tree = tinyfs_get_tree, }; static struct file_system_type tinyfs_fs_type = { .owner = THIS_MODULE, .name = "tinyfs", .init_fs_context = generic_init_fs_context, .parameters = NULL, .fs_context_ops = &tinyfs_context_ops, .kill_sb = kill_litter_super, }; /* ====================== 模块加载/卸载 ====================== */ static int __init tinyfs_init(void) { int ret; ret = register_filesystem(&tinyfs_fs_type); if (ret == 0) pr_info("tinyfs: registered\n"); else pr_err("tinyfs: register failed: %d\n", ret); return ret; } static void __exit tinyfs_exit(void) { unregister_filesystem(&tinyfs_fs_type); pr_info("tinyfs: unregistered\n"); } module_init(tinyfs_init); module_exit(tinyfs_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Demo"); MODULE_DESCRIPTION("Extremely minimal in-memory filesystem for learning");
-
Mount namespace + per-process root 每个进程(或进程组)可以有自己的根文件系统视图(pivot_root、容器最核心依赖)。
-
叠加文件系统天然支持 overlayfs、unionfs、ecryptfs、fscrypt 等都是在 VFS 层叠加实现的,而不是改 ext4。
常见文件系统在 VFS 中的定位对比
| 文件系统类型 | 是否有真实磁盘块 | super_block 来源 | inode 是否持久化 | 典型场景 | VFS 难度 |
|---|---|---|---|---|---|
| ext4 / xfs / btrfs | 是 | 读磁盘 | 是 | 根分区、数据盘 | ★★★ |
| tmpfs / ramfs | 否 | 内存生成 | 否(内存) | /tmp、shm | ★★ |
| procfs / sysfs | 否 | 内存动态生成 | 否 | 进程信息、内核参数 | ★★☆ |
| debugfs / tracefs | 否 | 内存动态 | 否 | 调试、tracing | ★★☆ |
| overlayfs | 否(中间层) | 内存 + 下层 | 部分 | 容器镜像层 | ★★★★ |
| nfs / cephfs | 网络 | 网络协议 | 部分缓存 | 网络共享存储 | ★★★★☆ |
1. Linux的文件类型(File Types)
Linux文件系统支持多种类型,这些类型在创建时通过系统调用(如mknod()、mkdir()、mkfifo()等)指定,并在inode的i_mode字段中用位掩码表示(例如,S_IFREG for regular file)。常见类型如下:
- 普通文件(Regular File):
- 表示:ls -l 显示 -。
- 描述:存储实际数据的文件,如文本文件、二进制可执行文件、图片等。数据按字节序列组织,支持随机访问(lseek())。
- 内核机制:数据存储在文件系统的块中(通常4KB块大小)。VFS通过文件系统的特定操作(如ext4的ext4_file_operations)处理I/O。内核使用页缓存(Page Cache)缓冲数据,提高读写效率。
- 示例:/etc/passwd(文本)、/bin/ls(二进制)。
- 特性:支持内存映射(mmap()),可以被多个进程共享读写。
- 目录(Directory):
- 表示:d。
- 描述:本质上是一个特殊文件,内容是文件名的列表及其inode映射(像一个键值对数据库)。
- 内核机制:通过readdir()系统调用遍历。目录inode指向目录项(dentry)缓存,优化路径解析。
- 示例:/home。
- 特性:不允许直接read()/write()内容,只能通过opendir()/readdir()访问。
- 符号链接(Symbolic Link):
- 表示:l。
- 描述:指向另一个文件或目录的“软链接”,内容是目标路径字符串。
- 内核机制:VFS在打开时解析链接(最多40层嵌套,避免循环)。不同于硬链接(hard link),符号链接有独立inode,不增加目标引用计数。
- 示例:ln -s /target /link。
- 特性:如果目标不存在,会出现“悬空链接”(dangling link)。
- 管道(Named Pipe 或 FIFO):
- 表示:p。
- 描述:用于进程间通信(IPC)的命名管道,支持单向数据流。
- 内核机制:通过mkfifo()创建。读写阻塞直到另一端打开。内部使用环形缓冲区(pipe buffer,通常64KB)。
- 示例:mkfifo mypipe。
- 特性:匿名管道(pipe()系统调用)不显示在文件系统中,但命名管道是持久的。
- 套接字(Socket):
- 表示:s。
- 描述:用于网络或本地IPC的端点,如Unix域套接字。
- 内核机制:通过socket()创建,绑定到文件描述符。VFS将套接字操作路由到网络栈(如TCP/IP)。
- 示例:/run/mysqld/mysqld.sock(MySQL套接字)。
- 特性:支持send()/recv(),但也可以用read()/write()。
- 设备文件(Device File):
- 这类是设备类型的核心,分为字符设备(Character Device)和块设备(Block Device)。设备文件不存储数据,而是作为设备驱动的入口点。它们位于/dev目录下,由udev或mknod()创建。inode的i_rdev字段存储主次设备号(major/minor number),主号标识驱动类型(如8 for SCSI磁盘),次号标识具体实例。
- 字符设备:
- 表示:c。
- 描述:按字符(字节)流处理I/O的设备,支持顺序访问,不支持随机寻址(lseek()通常无效)。
- 示例:/dev/tty(终端)、/dev/null(黑洞设备)、/dev/urandom(随机数生成器)、/dev/input/mouse(鼠标)。
- 块设备:
- 表示:b。
- 描述:按固定大小块(通常512B或4KB)处理I/O的设备,支持随机访问。
- 示例:/dev/sda(硬盘)、/dev/nvme0n1(NVMe SSD)、/dev/loop0(循环设备,用于挂载镜像)。
- 字符设备:
- 这类是设备类型的核心,分为字符设备(Character Device)和块设备(Block Device)。设备文件不存储数据,而是作为设备驱动的入口点。它们位于/dev目录下,由udev或mknod()创建。inode的i_rdev字段存储主次设备号(major/minor number),主号标识驱动类型(如8 for SCSI磁盘),次号标识具体实例。
设备文件是文件类型中的子集,但它们桥接了硬件和用户空间。内核通过设备驱动(模块,如nvme.ko)注册文件操作函数(file_operations结构体),如read_iter()、write_iter()。
socket
socket 在 VFS 层面确实是“伪装”成文件的,它完整地走 VFS 的 open/read/write/close/poll 等路径,但它并不实现所有常规文件系统会实现的那些操作。
1. socket 的 VFS 身份
- 类型:S_IFSOCK(在 inode->i_mode 里)
- 创建方式:不是通过 VFS 的 create/mknod/lookup,而是直接通过 sys_socket() / sock_create() → sock_alloc() → new_inode_pseudo() → 分配一个匿名 inode(没有 dentry 绑定到任何文件系统目录树)
- 关键标志:inode->i_sock = 1(老内核有这个字段,现在更多靠 S_IFSOCK 判断)
2. socket 的 file_operations
socket 使用的 file_operations 是全局唯一的:socket_file_ops(定义在 net/socket.c)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE, // 其实是 kernel 的核心模块
.llseek = no_llseek, // socket 不支持 lseek
.read_iter = sock_read_iter, // readv/read
.write_iter = sock_write_iter, // writev/write
.poll = sock_poll, // poll / select / epoll
.ioctl = sock_ioctl, // 大量 ioctl,比如 SIOCGIFCONF 等
.mmap = sock_mmap, // 支持内存映射(packet socket 等特殊场景)
.open = sock_no_open, // 不需要额外的 open 回调
.release = sock_close, // close(fd) 最终走到这里 → sock_release
.fasync = sock_fasync, // SIGIO 支持
.sendpage = sock_sendpage, // sendfile 支持(老接口)
.splice_write = sock_splice_eof, // splice 到 socket
// 以下很多是 .no_ 开头的占位函数(故意返回 -EINVAL)
.read = do_sync_read, // 兼容老接口,转到 read_iter
.write = do_sync_write,
.get_unmapped_area = sock_get_unmapped_area,
...
};
3. 常见 POSIX 接口在 socket 上的支持情况
| POSIX 操作 | 系统调用 | socket 是否支持 | 对应实现函数 | 备注 / 常见返回值 |
|---|---|---|---|---|
| open | socket(), accept() | 是 | sock_alloc + sock_map_fd | socket() 本身不走 open(),但 accept() 返回的 fd 走 VFS open 流程 |
| read / readv | read(), readv() | 是 | sock_read_iter | 正常读接收缓冲区数据 |
| write / writev | write(), writev() | 是 | sock_write_iter | 正常发到发送缓冲区 |
| close | close() | 是 | sock_close → sock_release | 释放 socket 对象 |
| lseek | lseek() | 否 | no_llseek → -ESPIPE | socket 没有“文件偏移量”概念 |
| mmap | mmap() | 部分支持 | sock_mmap | 只在 packet socket、某些 AF_UNIX 等支持,普通 tcp/udp 不行 |
| fsync / fdatasync | fsync() | 否 | 无实现 → -EINVAL | socket 不需要刷盘 |
| fallocate | fallocate() | 否 | 无 | -EOPNOTSUPP |
| ftruncate | ftruncate() | 否 | 无 | -EINVAL |
| stat / fstat | stat(), fstat() | 是 | sock_getattr | 返回固定信息(大小通常是 0,块设备信息无意义) |
| poll / select | poll(), select() | 是 | sock_poll | 非常重要!支持 POLLIN/POLLOUT/POLLERR 等 |
| epoll | epoll_ctl/wait | 是 | 通过 poll 桥接 | epoll 完全支持 |
| sendfile | sendfile() | 部分支持 | sock_sendpage | 支持,但效率不如 splice |
| splice | splice(), vmsplice() | 部分支持 | sock_splice_eof 等 | 可以从 pipe → socket,或 socket → pipe |
| ioctl | ioctl() | 是(非常多) | sock_ioctl | 支持 SIOCxxx 系列网络 ioctl(获取接口、MTU、路由等) |
4. socket 的 inode_operations
socket 的 inode_operations 基本是极简实现(或直接用 no_xxx 占位),因为 socket 不参与目录树操作:
- .lookup / .create / .mkdir / .unlink / .symlink → 全部不支持(-ENOTDIR 或 -EOPNOTSUPP)
- .setattr / .getattr → 有简单实现(主要是返回固定属性)
- .permission → 基本放通
socket 几乎不依赖 inode_operations,因为它不走路径查找流程(除了 unix domain socket 的文件系统路径那种特殊情况)。
总结一句话
socket 在 VFS 里是“半文件”:
- 支持:open/read/write/close/poll/epoll/ioctl/sendfile/splice/stat 等“流式”操作
- 不支持:lseek、truncate、mmap(大部分情况)、fsync、fallocate、目录相关操作(mkdir/readdir 等)