Linux内核FS子系统深度理解开发
第一阶段 从内至外视角
第一部分:VFS核心架构
1.1 文件系统三层架构
Linux文件系统架构 │ ├── 【系统调用层】fs/open.c, fs/read_write.c │ ├── sys_open() / sys_read() / sys_write() / sys_close() │ ├── sys_mkdir() / sys_rmdir() / sys_unlink() │ └── sys_stat() / sys_fstat() / sys_lstat() │ ├── 【VFS抽象层】核心数据结构 │ │ │ ├── struct super_block (超级块) │ │ ├── struct list_head s_list # 超级块链表 │ │ ├── struct file_system_type *s_type # 文件系统类型 │ │ ├── struct super_operations *s_op # 超级块操作 │ │ └── struct list_head s_instances # 同类实例链表 │ │ │ ├── struct inode (索引节点) │ │ ├── struct inode_operations *i_op # inode操作 │ │ ├── struct file_operations *i_fop # 文件操作 │ │ ├── struct super_block *i_sb # 所属超级块 │ │ ├── loff_t i_size # 文件大小 │ │ ├── struct timespec i_atime/mtime/ctime # 时间戳 │ │ └── struct address_space *i_mapping # 页缓存映射 │ │ │ ├── struct dentry (目录项) │ │ ├── struct inode *d_inode # 关联的inode │ │ ├── struct dentry *d_parent # 父目录项 │ │ ├── struct qstr d_name # 目录项名称 │ │ ├── struct list_head d_subdirs # 子目录链表 │ │ ├── struct dentry_operations *d_op # 目录项操作 │ │ └── struct super_block *d_sb # 超级块 │ │ │ ├── struct file (文件对象) │ │ ├── struct path f_path # 文件路径 │ │ ├── struct inode *f_inode # 关联的inode │ │ ├── const struct file_operations *f_op # 文件操作 │ │ ├── loff_t f_pos # 当前文件位置 │ │ └── fmode_t f_mode # 打开模式 │ │ │ └── struct vfsmount (挂载点) │ ├── struct dentry *mnt_root # 挂载根目录 │ ├── struct super_block *mnt_sb # 超级块 │ └── struct vfsmount *mnt_parent # 父挂载点 │ └── 【具体文件系统层】 ├── 磁盘文件系统: ext2/ext3/ext4, xfs, btrfs ├── 虚拟文件系统: procfs, sysfs, tmpfs, debugfs ├── 网络文件系统: NFS, CIFS, 9p └── 特殊文件系统: sockfs, pipefs, futexfs
1.2 VFS虚函数表结构
1.2.1 超级块操作表 (struct super_operations)
struct super_operations {
/* 在给定的超级块下创建并初始化一个新的索引节点对象 */
struct inode *(*alloc_inode)(struct super_block *sb);
/* 销毁索引节点对象 */
void (*destroy_inode)(struct inode *);
/* 从磁盘上读取索引节点,填充内存中的inode对象 */
void (*read_inode)(struct inode *);
/* 将inode写回磁盘 */
void (*write_inode)(struct inode *, struct writeback_control *wbc);
/* 释放超级块 */
void (*put_super)(struct super_block *);
/* 同步超级块到磁盘 */
int (*sync_fs)(struct super_block *sb, int wait);
/* 统计文件系统状态 */
int (*statfs)(struct dentry *, struct kstatfs *);
/* 重新挂载文件系统 */
int (*remount_fs)(struct super_block *, int *, char *);
/* 清除inode (最后一个引用释放时) */
void (*clear_inode)(struct inode *);
/* 在文件系统上显示挂载选项 */
int (*show_options)(struct seq_file *, struct dentry *);
/* 配额管理 */
int (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
int (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
/* 预读inode */
void (*readahead)(struct readahead_control *, struct dentry *);
};
1.2.2 索引节点操作表 (struct inode_operations)
struct inode_operations {
/* 查找目录中的dentry对应的inode */
struct dentry *(*lookup)(struct inode *, struct dentry *, unsigned int);
/* 创建普通文件 */
int (*create)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t, bool);
/* 创建硬链接 */
int (*link)(struct dentry *, struct inode *, struct dentry *);
/* 删除硬链接 */
int (*unlink)(struct inode *, struct dentry *);
/* 创建符号链接 */
int (*symlink)(struct mnt_idmap *, struct inode *, struct dentry *, const char *);
/* 创建目录 */
int (*mkdir)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t);
/* 删除目录 */
int (*rmdir)(struct inode *, struct dentry *);
/* 重命名文件/目录 */
int (*rename)(struct mnt_idmap *, struct inode *, struct dentry *,
struct inode *, struct dentry *, unsigned int);
/* 设置属性 (chmod, chown等) */
int (*setattr)(struct mnt_idmap *, struct dentry *, struct iattr *);
/* 获取属性 */
int (*getattr)(struct mnt_idmap *, const struct path *, struct kstat *, u32, unsigned int);
/* 设置扩展属性 (xattr) */
int (*setxattr)(struct mnt_idmap *, struct dentry *, const char *,
const void *, size_t, int);
/* 获取扩展属性 */
ssize_t (*getxattr)(struct dentry *, const char *, void *, size_t);
/* 列出扩展属性 */
ssize_t (*listxattr)(struct dentry *, char *, size_t);
/* 原子打开+创建 (支持NFS等网络文件系统) */
struct dentry *(*atomic_open)(struct inode *, struct dentry *, struct file *,
unsigned, umode_t);
};
1.2.3 文件操作表 (struct file_operations)
struct file_operations {
struct module *owner;
/* 文件偏移定位 */
loff_t (*llseek)(struct file *, loff_t, int);
/* 读取文件 */
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
/* 写入文件 */
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
/* 就绪查询 (用于poll/select/epoll) */
__poll_t (*poll)(struct file *, struct poll_table_struct *);
/* 异步I/O */
ssize_t (*read_iter)(struct kiocb *, struct iov_iter *);
ssize_t (*write_iter)(struct kiocb *, struct iov_iter *);
/* 打开文件 */
int (*open)(struct inode *, struct file *);
/* 释放文件 (关闭) */
int (*release)(struct inode *, struct file *);
/* 同步到磁盘 (fsync) */
int (*fsync)(struct file *, loff_t, loff_t, int);
/* 内存映射 */
int (*mmap)(struct file *, struct vm_area_struct *);
/* 文件锁 */
int (*flock)(struct file *, int, struct file_lock *);
/* 建议性锁 */
int (*lock)(struct file *, int, struct file_lock *);
/* 文件系统特定操作 (ioctl) */
long (*unlocked_ioctl)(struct file *, unsigned int, unsigned long);
long (*compat_ioctl)(struct file *, unsigned int, unsigned long);
/* fallocate (预分配空间) */
long (*fallocate)(struct file *, int, loff_t, loff_t);
/* 复制文件范围 (reflink) */
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int);
/* 克隆/去重 */
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t, u64);
int (*dedupe_file_range)(struct file *, loff_t, struct file *, loff_t, u64);
/* 重新映射文件范围 (用于数据块移动) */
loff_t (*remap_file_range)(struct file *, loff_t, struct file *, loff_t, loff_t, unsigned int);
/* 打开目录 (用于iterate_shared) */
int (*iterate_shared)(struct file *, struct dir_context *);
};
1.3 open/read系统调用完整流程
1.3.1 sys_open调用链
sys_open() 完整调用链
│
├── 【系统调用入口】 fs/open.c
│ └── SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
│ └── do_sys_open(AT_FDCWD, filename, flags, mode)
│
├── 【路径解析】 fs/namei.c
│ │
│ ├── do_sys_open()
│ │ └── do_filp_open()
│ │ ├── path_openat() # 路径解析核心函数
│ │ │ ├── set_nameidata() # 设置路径解析上下文
│ │ │ ├── link_path_walk() # 逐级解析路径组件
│ │ │ │ ├── walk_component() # 解析单个路径组件
│ │ │ │ │ ├── lookup_fast() # 从dcache快速查找
│ │ │ │ │ └── lookup_slow() # 调用文件系统lookup
│ │ │ │ │ └── inode->i_op->lookup() # 实际文件系统操作
│ │ │ │ └── handle_dots() # 处理"."和".."
│ │ │ ├── do_last() # 处理最后一个路径组件
│ │ │ │ ├── lookup_open() # 查找或创建文件
│ │ │ │ │ └── atomic_open() # 原子打开+创建
│ │ │ │ ├── vfs_open() # 调用文件系统open
│ │ │ │ │ └── file->f_op->open() # 实际文件系统open
│ │ │ │ └── may_open() # 权限检查
│ │ │ └── terminate_walk() # 清理路径解析状态
│ │ └── alloc_file() # 分配struct file
│ │
│ └── 关键数据结构流转
│ └── struct nameidata {
│ struct path path; # 当前路径
│ struct qstr last; # 最后一个组件
│ struct dentry *root; # 根目录
│ unsigned int flags; # LOOKUP_*标志
│ };
│
├── 【文件描述符管理】 fs/file.c
│ └── fd_install() # 将file绑定到文件描述符
│
└── 【返回用户态】
└── 返回 fd (文件描述符)
1.3.2 sys_read调用链
sys_read() 完整调用链 │ ├── 【系统调用入口】 fs/read_write.c │ └── SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count) │ └── ksys_read(fd, buf, count) │ └── vfs_read(file, buf, count, &file->f_pos) │ ├── 【VFS层】 fs/read_write.c │ └── vfs_read() │ ├── 权限检查: file->f_mode & FMODE_READ │ ├── 文件偏移检查 │ └── 调用文件系统读函数 │ ├── 如果定义了read_iter → file->f_op->read_iter() │ └── 否则调用 → file->f_op->read() │ ├── 【页缓存层】 mm/filemap.c (普通文件) │ │ │ ├── generic_file_read_iter() # 通用读实现 │ │ └── filemap_read() │ │ ├── filemap_get_pages() # 获取页缓存页 │ │ │ ├── find_get_page() # 查找页缓存 │ │ │ └── page_cache_sync_readahead() # 预读 │ │ ├── copy_page_to_iter() # 拷贝到用户空间 │ │ └── filemap_fault() # 缺页处理 │ │ │ └── address_space_operations │ ├── .readpage = ext4_readpage │ ├── .readahead = ext4_readahead │ └── .bmap = ext4_bmap │ ├── 【实际文件系统层】 fs/ext4/file.c │ │ │ ├── ext4_file_read_iter() │ │ ├── 检查是否direct I/O │ │ │ ├── 是 → ext4_dio_read_iter() │ │ │ └── 否 → generic_file_read_iter() │ │ └── 更新访问时间 (atime) │ │ │ └── ext4_readpage() # 读取单个页面 │ ├── ext4_mpage_readpages() # 多页读取 │ │ ├── ext4_map_blocks() # 逻辑块→物理块映射 │ │ ├── do_mpage_readpage() # 实际读取 │ │ └── submit_bio() # 提交块I/O请求 │ └── ext4_bio_write_page() # 写回脏页 │ ├── 【块设备层】 block/blk-core.c │ │ │ ├── submit_bio() # 提交bio到块层 │ │ ├── generic_make_request() # 通用请求构造 │ │ ├── blk_mq_submit_bio() # 多队列提交 │ │ └── blk_mq_sched_insert_request() # 插入调度队列 │ │ │ └── 块设备驱动处理 │ └── scsi_request_fn() # SCSI/ATA驱动 │ └── 【返回用户态】 └── 返回读取字节数
第二部分:文件系统类型源码库树形分析
2.1 主要文件系统源码目录结构
fs/ (Linux内核文件系统源码根目录) │ ├── 1. 虚拟文件系统核心 │ ├── open.c # open/close系统调用 │ ├── read_write.c # read/write系统调用 │ ├── file.c # 文件描述符管理 │ ├── namei.c # 路径查找核心 │ ├── super.c # 超级块管理 │ ├── inode.c # inode管理 │ ├── dentry.c # dcache目录项缓存 │ ├── namespace.c # 命名空间和挂载 │ ├── mount.h # 挂载相关 │ └── stat.c # stat/fstat系统调用 │ ├── 2. 页缓存与I/O │ ├── filemap.c # 页缓存核心 │ ├── buffer.c # 块缓冲层 │ ├── iomap.c # iomap框架 (现代块映射) │ ├── direct-io.c # Direct I/O实现 │ ├── readahead.c # 预读机制 │ ├── writeback.c # 回写机制 │ └── splice.c # splice系统调用 │ ├── 3. 内存管理接口 │ ├── mmap.c # 内存映射 │ └── madvise.c # madvise系统调用 │ ├── 4. 扩展属性与锁 │ ├── xattr.c # 扩展属性 │ └── locks.c # 文件锁 │ ├── 5. 磁盘文件系统 │ ├── ext2/ # Second Extended FS │ │ ├── ext2.h │ │ ├── super.c │ │ ├── inode.c │ │ ├── file.c │ │ ├── dir.c │ │ └── balloc.c # 块分配 │ │ │ ├── ext4/ # Fourth Extended FS │ │ ├── ext4.h │ │ ├── ext4_jbd2.h │ │ ├── super.c # 超级块操作 │ │ ├── inode.c # inode操作 │ │ ├── file.c # 文件操作 │ │ ├── namei.c # 目录查找 │ │ ├── dir.c # 目录操作 │ │ ├── extent.c # extent树管理 │ │ ├── mballoc.c # 多块分配器 │ │ ├── ialloc.c # inode分配 │ │ ├── block_validity.c │ │ ├── migrate.c │ │ ├── resize.c │ │ └── sysfs.c │ │ │ ├── xfs/ # XFS (高性能64位) │ │ ├── xfs_super.c │ │ ├── xfs_inode.c │ │ ├── xfs_bmap.c │ │ ├── xfs_alloc.c │ │ └── xfs_log.c │ │ │ ├── btrfs/ # B-tree FS (CoW) │ │ ├── super.c │ │ ├── extent-tree.c │ │ ├── file.c │ │ └── compression.c │ │ │ └── fat/ # FAT12/16/32 │ ├── fat.h │ ├── namei_msdos.c │ ├── file.c │ └── dir.c │ ├── 6. 虚拟文件系统 │ ├── proc/ # /proc 进程信息 │ │ ├── proc_root.c │ │ ├── proc_pid.c │ │ ├── inode.c │ │ └── proc_net.c │ │ │ ├── sysfs/ # /sys 系统信息 │ │ ├── sysfs.h │ │ ├── file.c │ │ ├── dir.c │ │ └── symlink.c │ │ │ ├── devfs/ # 设备文件系统 │ │ ├── devfs_core.c │ │ └── devfs_util.c │ │ │ ├── tmpfs/ # 内存文件系统 │ │ ├── shmem.c │ │ └── inode.c │ │ │ └── debugfs/ # 调试文件系统 │ ├── file.c │ ├── inode.c │ └── debugfs.c │ ├── 7. 网络文件系统 │ ├── nfs/ # NFS客户端 │ │ ├── nfs3proc.c │ │ ├── nfs4proc.c │ │ ├── nfs4state.c │ │ └── nfs4xdr.c │ │ │ ├── nfsd/ # NFS服务端 │ │ ├── nfs4proc.c │ │ ├── nfs4state.c │ │ ├── vfs.c │ │ └── export.c │ │ │ └── cifs/ # SMB/CIFS │ ├── cifsfs.c │ ├── file.c │ └── dir.c │ └── 8. 其他文件系统 ├── fuse/ # FUSE (用户空间文件系统) │ ├── dev.c │ ├── file.c │ ├── dir.c │ └── inode.c │ ├── overlayfs/ # 联合挂载 │ ├── overlayfs.h │ ├── file.c │ ├── dir.c │ └── inode.c │ ├── squashfs/ # 压缩只读文件系统 │ ├── squashfs_fs.h │ ├── file.c │ ├── inode.c │ └── block.c │ └── ramfs/ # 简单内存文件系统 ├── file-mmu.c └── inode.c
第三部分:Rootfs与磁盘布局树形分析
3.1 rootfs初始化与挂载流程
rootfs初始化完整流程
│
├── 【阶段1: 注册rootfs文件系统】
│ │
│ ├── init_rootfs() # rootfs注册入口
│ │ └── register_filesystem(&rootfs_fs_type)
│ │
│ └── rootfs_fs_type结构体
│ struct file_system_type rootfs_fs_type = {
│ .name = "rootfs",
│ .read_super = ramfs_read_super, # 使用ramfs实现
│ .fs_flags = FS_NOMOUNT | FS_LITTER,
│ .owner = THIS_MODULE,
│ };
│
├── 【阶段2: 创建VFS根目录并挂载rootfs】
│ │
│ ├── init_mount_tree()
│ │ │
│ │ ├── 1. do_kern_mount("rootfs", 0, "rootfs", NULL)
│ │ │ │
│ │ │ ├── alloc_vfsmnt() # 分配vfsmount结构
│ │ │ │
│ │ │ ├── get_sb_nodev() # 分配超级块
│ │ │ │ └── ramfs_read_super() # rootfs的read_super
│ │ │ │ │
│ │ │ │ ├── ramfs_get_inode() # 分配inode
│ │ │ │ │ ├── i_op = &ramfs_dir_inode_operations
│ │ │ │ │ └── i_fop = &dcache_dir_ops
│ │ │ │ │
│ │ │ │ ├── d_alloc_root() # 创建根dentry
│ │ │ │ └── inode->i_sb = sb
│ │ │ │
│ │ │ └── 返回mnt (vfsmount结构)
│ │ │
│ │ ├── 2. 创建namespace
│ │ │ └── init_task.namespace = namespace
│ │ │
│ │ └── 3. 设置当前进程的根目录
│ │ ├── set_fs_root(current->fs, mnt, mnt->mnt_root)
│ │ └── set_fs_pwd(current->fs, mnt, mnt->mnt_root)
│ │
│ └── 结果数据结构关系
│ ┌─────────────────────────────────────────────┐
│ │ vfsmount (mnt) │
│ │ ├── mnt_root → dentry (根目录) │
│ │ ├── mnt_sb → super_block │
│ │ └── mnt_parent → 指向自身 │
│ └─────────────────────────────────────────────┘
│ │
│ ▼
│ ┌─────────────────────────────────────────────┐
│ │ super_block (sb) │
│ │ ├── s_type → &rootfs_fs_type │
│ │ └── s_op → &ramfs_ops │
│ └─────────────────────────────────────────────┘
│ │
│ ▼
│ ┌─────────────────────────────────────────────┐
│ │ dentry (根目录) │
│ │ ├── d_inode → inode │
│ │ ├── d_parent → 指向自身 │
│ │ └── d_name = "/" │
│ └─────────────────────────────────────────────┘
│ │
│ ▼
│ ┌─────────────────────────────────────────────┐
│ │ inode (根目录inode) │
│ │ ├── i_op = &ramfs_dir_inode_operations │
│ │ ├── i_fop = &dcache_dir_ops │
│ │ └── i_sb → sb │
│ └─────────────────────────────────────────────┘
│
├── 【阶段3: 挂载其他文件系统到rootfs】
│ │
│ ├── /proc → procfs
│ │ └── mount("proc", "/proc", "proc", 0, NULL)
│ │
│ ├── /sys → sysfs
│ │ └── mount("sysfs", "/sys", "sysfs", 0, NULL)
│ │
│ ├── /dev → devtmpfs
│ │ └── mount("devtmpfs", "/dev", "devtmpfs", 0, NULL)
│ │
│ └── /run → tmpfs
│ └── mount("tmpfs", "/run", "tmpfs", 0, "size=10%")
│
└── 【阶段4: 切换根文件系统】
│
├── 内核加载initramfs
│ └── populate_rootfs() # 解压initramfs到rootfs
│
├── 启动init进程
│ └── run_init_process("/init")
│
└── 可选: pivot_root()切换到真实根文件系统
└── 将磁盘文件系统挂载到/root,然后切换
3.2 磁盘布局树形分析 (以EXT4为例)
EXT4磁盘布局 │ ├── 【块组布局】 │ └── 磁盘被划分为多个块组 (Block Group) │ │ │ ├── 块组0 (Group 0) │ │ ├── 超级块 (Super Block) - 块0 │ │ │ ├── s_inodes_count # inode总数 │ │ │ ├── s_blocks_count # 块总数 │ │ │ ├── s_free_blocks_count # 空闲块数 │ │ │ ├── s_free_inodes_count # 空闲inode数 │ │ │ ├── s_log_block_size # 块大小 (2^10+s_log_block_size) │ │ │ ├── s_magic = 0xEF53 # EXT4魔数 │ │ │ ├── s_mtime # 挂载时间 │ │ │ ├── s_wtime # 写入时间 │ │ │ └── s_first_data_block # 第一个数据块 │ │ │ │ │ ├── 组描述符表 (Group Descriptors) │ │ │ └── struct ext4_group_desc │ │ │ ├── bg_block_bitmap_lo # 块位图块号 │ │ │ ├── bg_inode_bitmap_lo # inode位图块号 │ │ │ ├── bg_inode_table_lo # inode表起始块 │ │ │ ├── bg_free_blocks_count # 空闲块数 │ │ │ └── bg_free_inodes_count # 空闲inode数 │ │ │ │ │ ├── 块位图 (Block Bitmap) │ │ │ └── 1位/块, 标记块是否空闲 │ │ │ │ │ ├── inode位图 (Inode Bitmap) │ │ │ └── 1位/inode, 标记inode是否空闲 │ │ │ │ │ ├── inode表 (Inode Table) │ │ │ └── struct ext4_inode [4KB/个] │ │ │ ├── i_mode # 文件类型和权限 │ │ │ ├── i_uid/gid # 所有者 │ │ │ ├── i_size # 文件大小 │ │ │ ├── i_atime/mtime/ctime # 时间戳 │ │ │ ├── i_blocks # 占用块数 │ │ │ ├── i_block[EXT4_N_BLOCKS] # 数据块指针 │ │ │ │ ├── 0-11: 直接块 │ │ │ │ ├── 12: 一级间接块 │ │ │ │ ├── 13: 二级间接块 │ │ │ │ └── 14: 三级间接块 │ │ │ ├── i_flags # 文件标志 │ │ │ ├── i_generation # 版本号 │ │ │ ├── i_file_acl # 扩展属性块 │ │ │ ├── i_size_high # 高32位大小 (大文件) │ │ │ └── i_checksum # 校验和 │ │ │ │ │ └── 数据块 (Data Blocks) │ │ └── 实际文件数据 │ │ │ ├── 块组1 (Group 1) │ │ ├── 超级块备份 (Super Block Backup) │ │ ├── 组描述符备份 │ │ ├── 块位图 │ │ ├── inode位图 │ │ ├── inode表 │ │ └── 数据块 │ │ │ └── 块组2 ~ N (类似结构,无超级块备份) │ ├── 【Extent树结构】 (EXT4引入,替代间接块) │ │ │ └── struct ext4_extent_header │ ├── eh_magic = 0xF30A │ ├── eh_entries # extent条目数 │ ├── eh_max # 最大条目数 │ ├── eh_depth # 树深度 │ └── eh_generation # 生成号 │ │ ├── 叶节点: struct ext4_extent │ │ ├── ee_block # 起始逻辑块号 │ │ ├── ee_len # 连续块数 │ │ ├── ee_start_lo # 起始物理块号(低32位) │ │ └── ee_start_hi # 起始物理块号(高16位) │ │ │ └── 索引节点: struct ext4_extent_idx │ ├── ei_block # 覆盖的逻辑块号 │ ├── ei_leaf_lo # 下一级节点块号(低32位) │ └── ei_leaf_hi # 下一级节点块号(高16位) │ └── 【日志区域】 (JBD2 - Journaling Block Device 2) │ ├── 日志超级块 (Journal Super Block) │ ├── s_sequence # 当前序列号 │ ├── s_start # 起始块号 │ └── s_errno # 错误码 │ ├── 描述符块 (Descriptor Block) │ └── 记录本次事务涉及的块号 │ ├── 数据块 (Data Blocks) │ └── 实际元数据/数据副本 │ └── 提交块 (Commit Block) └── 标记事务完成
第四部分:关键函数树形分析
4.1 ext4文件写入关键函数调用链
ext4文件写入完整调用链 │ ├── 【用户层】 │ └── write(fd, buf, count) │ ├── 【系统调用层】 │ └── sys_write() │ └── vfs_write() │ └── file->f_op->write_iter() │ ├── 【EXT4文件操作层】 fs/ext4/file.c │ │ │ └── ext4_file_write_iter() │ │ │ ├── 1. 检查O_DIRECT标志 │ │ │ │ │ ├── 如果iocb->ki_flags & IOCB_DIRECT │ │ │ └── ext4_dio_write_iter() # Direct I/O路径 │ │ │ ├── 对齐检查 │ │ │ ├── iomap_dio_rw() # 使用iomap框架 │ │ │ └── ext4_handle_inode_extension() # 处理文件扩展 │ │ │ │ │ └── 否则 (Buffered I/O) │ │ └── ext4_buffered_write_iter() │ │ │ └── 2. 更新atime (如果需要) │ └── file_accessed() │ ├── 【缓冲写路径】 fs/ext4/file.c │ │ │ └── ext4_buffered_write_iter() │ │ │ ├── ext4_ilock() # 锁inode │ │ │ ├── generic_perform_write() # 通用写函数 │ │ │ │ │ └── 循环处理每个页面 │ │ │ │ │ ├── a_ops->write_begin() = ext4_write_begin() │ │ │ │ │ │ │ ├── ext4_da_write_begin() # 延迟分配写开始 │ │ │ │ ├── 查找或分配页缓存页 │ │ │ │ ├── ext4_journal_start() # 启动事务 │ │ │ │ ├── ext4_map_blocks() # 块映射/分配 │ │ │ │ └── block_write_begin() │ │ │ │ │ │ │ └── 如果失败 → ext4_write_begin() │ │ │ └── 标准写开始路径 │ │ │ │ │ ├── 拷贝用户数据到页缓存 │ │ │ └── copy_page_from_iter_atomic() │ │ │ │ │ └── a_ops->write_end() = ext4_write_end() │ │ │ │ │ ├── ext4_da_write_end() # 延迟分配写结束 │ │ │ ├── generic_write_end() │ │ │ ├── 标记页为脏 │ │ │ └── ext4_journal_stop() # 提交事务 │ │ │ │ │ └── ext4_journal_stop() │ │ │ └── ext4_iunlock() # 解锁inode │ ├── 【块分配核心】 fs/ext4/extents.c │ │ │ └── ext4_map_blocks() # 逻辑块→物理块映射 │ │ │ ├── 1. 查找现有映射 │ │ └── ext4_ext_find_extent() # 遍历extent树 │ │ │ │ │ ├── 读取extent树节点 │ │ └── 返回包含目标块的extent │ │ │ ├── 2. 如果不存在且需要分配 │ │ └── ext4_ext_map_blocks() # 分配新块 │ │ │ │ │ ├── ext4_mb_new_blocks() # 多块分配器 │ │ │ ├── ext4_mb_regular_allocator() # 常规分配 │ │ │ │ ├── 遍历块组 │ │ │ │ ├── 检查块位图 │ │ │ │ └── 更新位图 │ │ │ └── ext4_mb_use_best_found() # 使用最佳匹配 │ │ │ │ │ ├── ext4_ext_create_new_leaf() # 创建新叶节点 │ │ │ │ │ └── ext4_ext_insert_extent() # 插入extent树 │ │ │ └── 3. 返回物理块号 │ ├── 【页缓存管理】 mm/filemap.c │ │ │ ├── find_get_page() # 查找页缓存 │ │ │ ├── __page_cache_alloc() # 分配新页面 │ │ │ ├── add_to_page_cache_lru() # 加入LRU缓存 │ │ │ └── mark_buffer_dirty() # 标记块为脏 │ ├── 【日志层】 fs/jbd2/ │ │ │ ├── jbd2_journal_start() # 开始事务 │ │ ├── 获取当前事务句柄 │ │ └── 增加事务引用计数 │ │ │ ├── jbd2_journal_get_write_access() # 获取块写权限 │ │ └── 将块加入事务元数据列表 │ │ │ ├── jbd2_journal_dirty_metadata() # 标记元数据脏 │ │ └── 加入提交列表 │ │ │ └── jbd2_journal_stop() # 停止事务 │ └── 触发日志写入 (如果达到阈值) │ └── 【回写机制】 fs/fs-writeback.c │ ├── writeback_sb_inodes() # 回写超级块的inode │ ├── 遍历脏inode链表 │ ├── __writeback_single_inode() # 回写单个inode │ │ ├── do_writepages() # 实际写页面 │ │ └── 调用 a_ops->writepages = ext4_writepages() │ └── 更新回写统计 │ └── wb_workfn() # 回写工作线程 └── 定期或达到阈值时触发回写
第五部分:epoll事件分发机制
5.1 epoll核心数据结构
epoll核心数据结构 │ ├── 【epoll实例】 struct eventpoll │ │ │ ├── struct rb_root rbr; # 红黑树,存储所有监视的fd │ ├── struct list_head rdllist; # 就绪队列 (双链表) │ ├── wait_queue_head_t wq; # 等待队列 (epoll_wait阻塞的进程) │ ├── struct list_head fds; # 文件描述符列表 │ ├── struct file *file; # 关联的文件对象 │ ├── int ovflist; # 溢出列表 │ └── struct user_struct *user; # 用户信息 │ ├── 【epoll条目】 struct epitem │ │ │ ├── struct rb_node rbn; # 红黑树节点 │ ├── struct list_head rdllink; # 就绪队列链接 │ ├── struct epoll_filefd ffd; # 关联的文件和fd │ │ ├── struct file *file │ │ └── int fd │ ├── struct eventpoll *ep; # 所属epoll实例 │ ├── struct list_head pwqlist; # 轮询等待队列链表 │ ├── struct epoll_event event; # 用户关注的事件 │ └── int nwait; # 等待队列计数 │ ├── 【轮询等待队列】 struct eppoll_entry │ │ │ ├── struct list_head llink; # 链接到epitem │ ├── struct epitem *base; # 指向epitem │ ├── wait_queue_entry_t wait; # 等待队列项 │ └── struct epoll_event *event; # 事件 │ └── 【文件操作】 struct file_operations epoll_fops ├── .poll = ep_eventpoll_poll # epoll本身可被poll ├── .release = ep_eventpoll_release # 释放 └── .unlocked_ioctl = ep_ioctl
5.2 epoll事件分发流程
epoll事件分发完整流程 │ ├── 【epoll_create】创建epoll实例 │ │ │ ├── SYSCALL_DEFINE1(epoll_create, int, size) │ │ └── epoll_create1(0) │ │ └── do_epoll_create() │ │ ├── 分配struct eventpoll │ │ ├── 初始化红黑树(rbr) │ │ ├── 初始化就绪队列(rdllist) │ │ ├── 初始化等待队列(wq) │ │ └── 返回文件描述符 │ │ │ └── 返回epoll实例fd │ ├── 【epoll_ctl】注册监视fd │ │ │ ├── SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd, struct epoll_event __user *, event) │ │ │ │ │ ├── 1. ep_find() # 在红黑树中查找epitem │ │ │ │ │ ├── 2. 根据op执行操作 │ │ │ │ │ │ │ ├── EPOLL_CTL_ADD │ │ │ │ ├── ep_insert() # 插入新epitem │ │ │ │ │ ├── 分配epitem │ │ │ │ │ ├── 初始化epitem │ │ │ │ │ ├── 添加到红黑树 (rb_insert_color) │ │ │ │ │ ├── 调用目标文件的poll函数 │ │ │ │ │ │ └── file->f_op->poll() │ │ │ │ │ │ │ │ │ │ │ │ │ └── 返回当前事件状态 │ │ │ │ │ │ │ │ │ │ │ │ │ ├── 如果有就绪事件 → 加入rdllist │ │ │ │ │ │ └── 同时注册等待队列回调 │ │ │ │ │ │ │ │ │ │ │ └── 注册回调: ep_ptable_queue_proc() │ │ │ │ │ └── 将epitem加入文件私有等待队列 │ │ │ │ │ │ │ │ │ └── 返回 │ │ │ │ │ │ │ ├── EPOLL_CTL_DEL │ │ │ │ └── ep_remove() # 从红黑树删除 │ │ │ │ │ │ │ └── EPOLL_CTL_MOD │ │ │ └── ep_modify() # 修改监视事件 │ │ │ │ │ └── 返回 │ │ │ └── 关键: 只有实现了poll的文件类型才能被epoll监视 │ ├── 【epoll_wait】等待事件 │ │ │ ├── SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events, int, maxevents, int, timeout) │ │ │ │ │ └── ep_poll() │ │ │ │ │ ├── 1. 检查就绪队列 (rdllist) │ │ │ └── 如果不为空,直接返回 │ │ │ │ │ ├── 2. 如果就绪队列为空 │ │ │ │ │ │ │ ├── 准备等待 │ │ │ │ └── init_waitqueue_entry() # 初始化等待队列项 │ │ │ │ │ │ │ ├── 加入等待队列 (wq) │ │ │ │ └── __add_wait_queue() │ │ │ │ │ │ │ ├── 设置超时 │ │ │ │ └── schedule_timeout() # 进程睡眠 │ │ │ │ │ │ │ └── 被唤醒后移除等待队列 │ │ │ │ │ └── 3. 返回就绪事件 │ │ └── ep_send_events() │ │ ├── 遍历rdllist │ │ ├── 将事件拷贝到用户空间 │ │ ├── 触发电平触发/边沿触发逻辑 │ │ │ ├── 电平触发 (LT): 保留在rdllist │ │ │ └── 边沿触发 (ET): 从rdllist移除 │ │ └── 返回事件数量 │ │ │ └── 返回就绪事件数 │ └── 【事件回调机制】 │ ├── 当被监视fd有事件发生时 │ │ │ └── 驱动调用 poll_wait() 注册的回调 │ │ │ └── ep_poll_callback() # 核心回调函数 │ │ │ ├── 1. 将epitem加入就绪队列 (rdllist) │ │ │ ├── 2. 检查是否有进程在epoll_wait等待 │ │ │ │ │ ├── 如果有等待进程 │ │ │ └── wake_up(&ep->wq) # 唤醒等待进程 │ │ │ │ │ └── 如果有进程在等待文件本身 │ │ └── wake_up(&ep->poll_wait) │ │ │ └── 返回 │ └── 说明: epoll的O(1)性能来自于: ├── 红黑树管理 → O(log n) 增删改 ├── 就绪队列 → O(1) 事件分发 └── 回调机制 → 主动通知,无需轮询
5.3 epoll惊群效应分析
epoll惊群效应
│
├── 【定义】
│ └── 多个进程等待同一个epoll实例,事件触发时所有进程被唤醒,
│ 但只有1个进程能成功处理事件,其他进程白白被唤醒。
│
├── 【原因分析】
│ │
│ ├── epoll_wait()中,多个进程同时调用epoll_wait
│ │ └── 都加入ep->wq等待队列
│ │
│ ├── 事件触发时
│ │ └── ep_poll_callback()调用wake_up(&ep->wq)
│ │ └── 唤醒所有等待进程
│ │
│ └── 只有一个进程能成功获取事件
│
├── 【解决方案】
│ │
│ ├── 方案1: EPOLLEXCLUSIVE标志 (Linux 4.5+)
│ │ └── epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event)
│ │ └── event.events |= EPOLLEXCLUSIVE
│ │ └── 只唤醒一个进程
│ │
│ ├── 方案2: SO_REUSEPORT socket选项
│ │ └── 允许多个socket绑定同一端口
│ │ └── 内核负载均衡分发连接
│ │
│ └── 方案3: 使用SO_REUSEADDR + accept锁
│ └── 进程间使用锁同步accept
│
└── 惊群示例代码
for (k = 0; k < PROCESS_NUM; k++) {
int pid = fork();
if (pid == 0) {
while (1) {
n = epoll_wait(efd, events, MAXEVENTS, -1);
printf("process %d return from epoll_wait!\n", getpid());
// 处理连接...
}
}
}
// 一个连接到达时,所有PROCESS_NUM个进程都会被唤醒
第六部分:调试流程与工具树形分析
6.1 文件系统调试流程树
文件系统调试决策树
│
├── 【第1层: 用户空间观察】
│ │
│ ├── 基本状态检查
│ │ ├── df -h # 磁盘空间使用
│ │ ├── df -i # inode使用
│ │ ├── mount # 挂载点状态
│ │ └── cat /proc/mounts # 内核挂载信息
│ │
│ ├── 文件系统统计
│ │ ├── iostat -x 1 # 磁盘I/O统计
│ │ ├── sar -d 1 # 设备活动报告
│ │ └── vmstat 1 # 内存和I/O统计
│ │
│ └── 性能分析
│ ├── blktrace # 块层追踪
│ └── fio # I/O性能测试
│
├── 【第2层: 内核跟踪点】
│ │
│ ├── trace-cmd / ftrace
│ │ │
│ │ ├── 启用文件系统跟踪点
│ │ │ echo 1 > /sys/kernel/tracing/events/ext4/ext4_writepages/enable
│ │ │ echo 1 > /sys/kernel/tracing/events/ext4/ext4_map_blocks/enable
│ │ │ echo 1 > /sys/kernel/tracing/events/vfs/vfs_read/enable
│ │ │ echo 1 > /sys/kernel/tracing/events/filemap/mm_filemap_add_to_page_cache/enable
│ │ │
│ │ ├── 查看追踪输出
│ │ │ cat /sys/kernel/tracing/trace_pipe
│ │ │
│ │ └── trace-cmd record -e ext4 -e vfs -e filemap -p function
│ │
│ ├── perf事件
│ │ │
│ │ ├── perf record -e ext4:ext4_* -a
│ │ ├── perf record -e block:block_rq_*
│ │ └── perf report
│ │
│ └── bpftrace动态追踪
│ │
│ ├── 追踪ext4_write_begin
│ │ bpftrace -e 'kprobe:ext4_write_begin { printf("%s: %d\n", comm, arg2); }'
│ │
│ └── 追踪文件打开
│ bpftrace -e 'kprobe:do_sys_open { printf("open: %s\n", str(arg1)); }'
│
├── 【第3层: 文件系统专用调试】
│ │
│ ├── debugfs (ext2/3/4专用)
│ │ │
│ │ ├── debugfs /dev/sda1
│ │ │ ├── stats # 文件系统统计
│ │ │ ├── ls / # 列出目录
│ │ │ ├── cat <inode> # 查看inode内容
│ │ │ ├── dump_extents <inode> # 查看extent树
│ │ │ ├── filefrag <file> # 查看文件碎片
│ │ │ ├── blocks <inode> # 查看块映射
│ │ │ └── find_free_block # 查找空闲块
│ │ │
│ │ └── 常用调试命令
│ │ debugfs -R "stat <2>" /dev/sda1 # 查看根目录inode
│ │ debugfs -R "dump_extents /path" # 查看extent
│ │ debugfs -R "logdump" /dev/sda1 # 查看日志
│ │
│ ├── xfs_db (XFS专用)
│ │ ├── xfs_db -c "sb" /dev/sda1
│ │ ├── xfs_db -c "blockget" -c "print"
│ │ └── xfs_db -c "frag" -c "info"
│ │
│ ├── btrfs工具
│ │ ├── btrfs inspect-internal dump-super /dev/sda1
│ │ ├── btrfs filesystem show
│ │ └── btrfs check
│ │
│ └── dumpe2fs (ext2/3/4)
│ └── dumpe2fs -h /dev/sda1 # 查看超级块信息
│
├── 【第4层: 块层调试】
│ │
│ ├── blktrace + blkparse
│ │ │
│ │ ├── blktrace -d /dev/sda -o trace
│ │ ├── blkparse -i trace > trace.txt
│ │ └── 分析I/O模式和延迟
│ │
│ ├── seekwatcher
│ │ └── 可视化I/O模式
│ │
│ └── iowatcher
│ └── 生成I/O时间线图
│
├── 【第5层: 内核调试器】
│ │
│ ├── kgdb + gdb
│ │ ├── 设置断点: break ext4_map_blocks
│ │ ├── 检查变量: p *inode
│ │ └── 单步执行
│ │
│ └── kdump / crash
│ └── 分析崩溃转储
│
└── 【第6层: 硬件层】
│
├── smartctl # 硬盘SMART信息
│ └── smartctl -a /dev/sda
│
└── 硬件诊断
├── 检查SATA/PCIe链路
├── 检查电源和线缆
└── 磁盘坏道检测: badblocks
6.2 调试工具树形图
文件系统调试工具全景 │ ├── 【监控工具】 │ │ │ ├── df / du │ │ ├── df -hT # 显示类型和人类可读大小 │ │ ├── du -sh * # 目录大小汇总 │ │ └── ncdu # 交互式磁盘使用分析 │ │ │ ├── iostat │ │ ├── iostat -x 1 # 扩展统计 (await, svctm, %util) │ │ ├── iostat -k 1 # KB/s统计 │ │ └── iostat -m 1 # MB/s统计 │ │ │ ├── pidstat │ │ └── pidstat -d 1 # 进程I/O统计 │ │ │ ├── dstat │ │ ├── dstat -d # 磁盘统计 │ │ ├── dstat -f # 完整统计 │ │ └── dstat -ta 6 # 每6秒更新 │ │ │ ├── glances │ │ └── 综合监控工具 │ │ │ ├── inotify-tools │ │ ├── inotifywait -m -r /path # 递归监控文件变化 │ │ └── inotifywatch # 统计文件事件 │ │ │ └── fatrace │ └── 追踪文件系统访问 │ ├── 【分析工具】 │ │ │ ├── strace │ │ ├── strace -e open,read,write # 追踪特定系统调用 │ │ ├── strace -p PID # 附加到进程 │ │ └── strace -tt -T # 时间戳和耗时 │ │ │ ├── ltrace │ │ └── 追踪库函数调用 │ │ │ ├── perf │ │ ├── perf stat # 性能计数器 │ │ ├── perf record # 记录性能数据 │ │ ├── perf report # 生成报告 │ │ └── perf top # 实时性能分析 │ │ │ ├── ftrace │ │ ├── function tracer # 函数调用追踪 │ │ ├── function_graph tracer # 函数图追踪 │ │ └── wakeup tracer # 唤醒延迟追踪 │ │ │ └── trace-cmd │ ├── trace-cmd record # 记录跟踪数据 │ ├── trace-cmd report # 报告生成 │ └── trace-cmd list # 列出跟踪点 │ ├── 【诊断工具】 │ │ │ ├── fsck家族 │ │ ├── e2fsck (ext2/3/4) │ │ │ ├── e2fsck -p # 自动修复 │ │ │ ├── e2fsck -n # 只读检查 │ │ │ └── e2fsck -y # 自动回答yes │ │ ├── xfs_repair (XFS) │ │ ├── btrfs check (btrfs) │ │ └── fsck.fat (FAT) │ │ │ ├── debugfs │ │ ├── debugfs -w /dev/sda1 # 读写模式 │ │ ├── debugfs -c /dev/sda1 # 灾难模式 │ │ ├── debugfs -R "command" # 单命令执行 │ │ └── debugfs -f script.cmd # 批量执行 │ │ │ ├── blktrace │ │ ├── blktrace -d /dev/sda # 追踪块设备 │ │ └── blkparse # 解析blktrace输出 │ │ │ └── filefrag │ └── filefrag -v <file> # 查看文件碎片 │ ├── 【测试工具】 │ │ │ ├── fio │ │ ├── fio --filename=/dev/sda --rw=read --bs=4k --size=1G │ │ ├── fio --filename=/test --rw=randwrite --bs=4k --size=1G │ │ └── fio --filename=/test --rw=rw --bs=64k --size=10G │ │ │ ├── bonnie++ │ │ └── 文件系统基准测试 │ │ │ ├── iozone │ │ └── 综合I/O性能测试 │ │ │ ├── xfstests │ │ └── 文件系统一致性测试套件 │ │ │ └── ltp (Linux Test Project) │ └── 全面的Linux测试套件 │ └── 【可视化工具】 │ ├── iowatcher │ └── 生成blktrace数据的可视化图 │ ├── seekwatcher │ └── I/O模式可视化 │ ├── gnuplot │ └── 性能数据绘图 │ └── perf script └── 生成火焰图数据
第七部分:性能分析树形分析
7.1 文件系统性能指标体系
文件系统性能指标 │ ├── 【吞吐量指标】 │ ├── 顺序读吞吐量 (MB/s) │ ├── 顺序写吞吐量 (MB/s) │ ├── 随机读IOPS │ ├── 随机写IOPS │ ├── 混合读写吞吐量 │ └── 元数据操作速率 (create/delete/stat) │ ├── 【延迟指标】 │ ├── 读延迟 (平均/95th/99th) │ │ ├── 页缓存命中延迟 │ │ └── 磁盘I/O延迟 │ ├── 写延迟 (平均/95th/99th) │ │ ├── 异步写返回延迟 │ │ └── fsync/fdatasync延迟 │ ├── 元数据操作延迟 │ │ ├── open()延迟 │ │ ├── stat()延迟 │ │ └── unlink()延迟 │ └── 端到端应用延迟 │ ├── 【缓存效率指标】 │ ├── 页缓存命中率 │ ├── dentry缓存命中率 │ ├── inode缓存命中率 │ ├── 脏页比例 │ └── 回写效率 │ ├── 【空间利用率】 │ ├── 磁盘空间使用率 │ ├── inode使用率 │ ├── 文件碎片率 │ ├── 目录深度分布 │ └── 文件大小分布 │ └── 【资源消耗指标】 ├── CPU使用率 (系统态/用户态) ├── 内存占用 (页缓存/元数据缓存) ├── I/O带宽利用率 ├── 设备队列深度 └── 锁竞争统计
7.2 性能瓶颈定位流程
性能瓶颈定位决策树 │ ├── 【瓶颈分类】 │ │ │ ├── 类型1: 吞吐量不足 │ │ │ │ │ ├── 检查设备层 │ │ │ ├── iostat -x 1 │ │ │ │ ├── %util接近100% → 设备饱和 │ │ │ │ ├── await > 10ms → 设备延迟高 │ │ │ │ └── r_await/w_await差异 → 读写不平衡 │ │ │ └── blktrace分析 │ │ │ └── 检查I/O模式是否连续 │ │ │ │ │ ├── 检查文件系统层 │ │ │ ├── 碎片分析: filefrag -v │ │ │ ├── 挂载选项: mount │ │ │ │ ├── noatime → 减少atime更新 │ │ │ │ ├── nobarrier → 禁用屏障(谨慎) │ │ │ │ └── data=writeback → 提高写性能 │ │ │ └── 日志模式: tune2fs -l /dev/sda1 │ │ │ │ │ └── 检查页缓存 │ │ ├── free -h # 查看可用内存 │ │ ├── cat /proc/meminfo | grep -E "(Dirty|Writeback|Cached)" │ │ └── vmstat 1 # 观察bi/bo │ │ │ ├── 类型2: 延迟过高 │ │ │ │ │ ├── 分析I/O调度器 │ │ │ ├── cat /sys/block/sda/queue/scheduler │ │ │ ├── 选择合适调度器 │ │ │ │ ├── none (NVMe) │ │ │ │ ├── mq-deadline (SAS/SSD) │ │ │ │ └── bfq (机械硬盘) │ │ │ └── 调整队列深度 │ │ │ │ │ ├── 分析fsync行为 │ │ │ ├── strace -e fsync,fdatasync -T │ │ │ ├── 应用程序是否频繁fsync │ │ │ └── 考虑批量提交 │ │ │ │ │ └── 分析锁竞争 │ │ ├── perf lock record │ │ ├── perf lock report │ │ └── 检查inode锁竞争 │ │ │ └── 类型3: 缓存效率低 │ │ │ ├── 页缓存分析 │ │ ├── /proc/meminfo │ │ │ ├── Cached > 80% → 缓存充足 │ │ │ └── Dirty > 20% → 回写滞后 │ │ ├── 调整脏页阈值 │ │ │ ├── /proc/sys/vm/dirty_ratio │ │ │ └── /proc/sys/vm/dirty_background_ratio │ │ └── 预读效率 │ │ ├── readahead统计 │ │ └── 调整预读大小: blockdev --setra │ │ │ └── dcache/icache分析 │ ├── slabtop │ ├── cat /proc/slabinfo | grep dentry │ └── cat /proc/slabinfo | grep inode │ └── 【优化建议矩阵】 │ ├── 应用层优化 │ ├── 使用O_DIRECT绕过页缓存 (数据库场景) │ ├── 使用aio/liburing异步I/O │ ├── 批量操作减少系统调用 │ └── 使用mmap替代read/write │ ├── 文件系统层优化 │ ├── 挂载选项: noatime, nodiratime │ ├── 调整日志模式: data=writeback │ ├── 禁用访问时间更新 │ └── 使用ext4的fast_commit特性 │ ├── 块层优化 │ ├── 选择合适的I/O调度器 │ ├── 调整队列深度: /sys/block/sda/queue/nr_requests │ ├── 启用RQS (Request Queue Sort) │ └── 对齐分区到SSD擦除块 │ └── 硬件层优化 ├── 使用NVMe SSD替代SATA ├── RAID配置优化 (条带大小) └── 使用持久内存 (DAX模式)
第八部分:自定义文件系统实现框架
8.1 最小文件系统实现框架
/* 最小文件系统实现 - 基于内存的简单文件系统 */
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/pagemap.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
/* ========== 1. 文件系统私有数据结构 ========== */
struct myfs_inode_info {
struct inode vfs_inode;
char *data; /* 文件数据 */
size_t size; /* 文件大小 */
};
/* 从inode获取私有数据 */
static inline struct myfs_inode_info *MYFS_I(struct inode *inode)
{
return container_of(inode, struct myfs_inode_info, vfs_inode);
}
/* ========== 2. 超级块操作 ========== */
static struct inode *myfs_alloc_inode(struct super_block *sb)
{
struct myfs_inode_info *info;
info = kmalloc(sizeof(struct myfs_inode_info), GFP_KERNEL);
if (!info)
return NULL;
info->data = NULL;
info->size = 0;
return &info->vfs_inode;
}
static void myfs_free_inode(struct inode *inode)
{
struct myfs_inode_info *info = MYFS_I(inode);
kfree(info->data);
kfree(info);
}
static int myfs_write_inode(struct inode *inode, struct writeback_control *wbc)
{
/* 内存文件系统,无需写回磁盘 */
return 0;
}
static const struct super_operations myfs_sops = {
.alloc_inode = myfs_alloc_inode,
.free_inode = myfs_free_inode,
.write_inode = myfs_write_inode,
.statfs = simple_statfs,
.drop_inode = generic_delete_inode,
};
/* ========== 3. 索引节点操作 ========== */
static struct dentry *myfs_lookup(struct inode *parent, struct dentry *dentry,
unsigned int flags)
{
struct inode *inode = NULL;
const char *name = dentry->d_name.name;
/* 简单实现: 只支持根目录和普通文件 */
if (parent->i_ino == 1) {
/* 根目录下可以创建文件 */
inode = new_inode(parent->i_sb);
if (inode) {
inode->i_ino = get_next_ino();
inode->i_mode = S_IFREG | 0644;
inode->i_uid = current_fsuid();
inode->i_gid = current_fsgid();
inode->i_atime = inode->i_mtime = inode->i_ctime = current_time(inode);
inode->i_size = 0;
inode->i_op = &simple_file_inode_operations;
inode->i_fop = &myfs_file_ops;
struct myfs_inode_info *info = MYFS_I(inode);
info->size = 0;
info->data = NULL;
}
}
d_add(dentry, inode);
return NULL;
}
static const struct inode_operations myfs_dir_inode_ops = {
.lookup = myfs_lookup,
.create = myfs_create,
};
/* ========== 4. 文件操作 ========== */
static ssize_t myfs_read(struct file *filp, char __user *buf, size_t len,
loff_t *off)
{
struct inode *inode = file_inode(filp);
struct myfs_inode_info *info = MYFS_I(inode);
size_t read_size;
if (*off >= info->size)
return 0;
read_size = min_t(size_t, len, info->size - *off);
if (copy_to_user(buf, info->data + *off, read_size))
return -EFAULT;
*off += read_size;
return read_size;
}
static ssize_t myfs_write(struct file *filp, const char __user *buf, size_t len,
loff_t *off)
{
struct inode *inode = file_inode(filp);
struct myfs_inode_info *info = MYFS_I(inode);
loff_t new_size = *off + len;
char *new_data;
/* 扩展文件大小 */
if (new_size > info->size) {
new_data = krealloc(info->data, new_size, GFP_KERNEL);
if (!new_data)
return -ENOMEM;
info->data = new_data;
memset(info->data + info->size, 0, new_size - info->size);
info->size = new_size;
inode->i_size = new_size;
}
if (copy_from_user(info->data + *off, buf, len))
return -EFAULT;
*off += len;
return len;
}
static const struct file_operations myfs_file_ops = {
.read = myfs_read,
.write = myfs_write,
.llseek = default_llseek,
};
/* ========== 5. 文件系统类型注册 ========== */
static int myfs_fill_super(struct super_block *sb, void *data, int silent)
{
struct inode *root_inode;
sb->s_magic = 0x20241225;
sb->s_op = &myfs_sops;
sb->s_time_gran = 1;
/* 创建根目录inode */
root_inode = new_inode(sb);
if (!root_inode)
return -ENOMEM;
root_inode->i_ino = 1;
root_inode->i_mode = S_IFDIR | 0755;
root_inode->i_uid = current_fsuid();
root_inode->i_gid = current_fsgid();
root_inode->i_atime = root_inode->i_mtime = root_inode->i_ctime = current_time(root_inode);
root_inode->i_op = &myfs_dir_inode_ops;
root_inode->i_fop = &simple_dir_operations;
/* 创建根目录dentry */
sb->s_root = d_make_root(root_inode);
if (!sb->s_root)
return -ENOMEM;
return 0;
}
static struct dentry *myfs_mount(struct file_system_type *fs_type, int flags,
const char *dev_name, void *data)
{
return mount_nodev(fs_type, flags, data, myfs_fill_super);
}
static void myfs_kill_sb(struct super_block *sb)
{
kill_anon_super(sb);
}
static struct file_system_type myfs_fs_type = {
.name = "myfs",
.mount = myfs_mount,
.kill_sb = myfs_kill_sb,
.owner = THIS_MODULE,
};
/* ========== 6. 模块初始化和退出 ========== */
static int __init myfs_init(void)
{
return register_filesystem(&myfs_fs_type);
}
static void __exit myfs_exit(void)
{
unregister_filesystem(&myfs_fs_type);
}
module_init(myfs_init);
module_exit(myfs_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Simple memory-based filesystem");
8.2 文件系统数据处理Pipeline
自定义文件系统数据处理Pipeline │ ├── 【数据写入Pipeline】 │ │ │ ├── 用户层 │ │ └── write(fd, buf, len) │ │ │ ├── VFS层 │ │ └── vfs_write() → file->f_op->write_iter() │ │ │ ├── 文件系统层 (自定义) │ │ │ │ │ ├── myfs_write_iter() │ │ │ ├── 加锁 (inode->i_rwsem) │ │ │ │ │ │ │ ├── 检查文件大小限制 │ │ │ │ │ │ │ ├── 扩展文件 (如果需要) │ │ │ │ ├── myfs_expand_file() │ │ │ │ │ ├── 分配新块 │ │ │ │ │ ├── 更新块映射表 │ │ │ │ │ └── 更新inode大小 │ │ │ │ └── 更新inode时间戳 │ │ │ │ │ │ │ ├── 拷贝数据 (用户空间 → 内核空间) │ │ │ │ └── copy_from_user() │ │ │ │ │ │ │ ├── 写入实际存储 │ │ │ │ ├── 内存文件系统: memcpy到缓冲区 │ │ │ │ ├── 磁盘文件系统: 写入页缓存 │ │ │ │ └── 标记脏页 │ │ │ │ │ │ │ └── 更新inode元数据 │ │ │ │ │ └── 解锁 │ │ │ ├── 页缓存层 (磁盘文件系统) │ │ ├── generic_perform_write() │ │ ├── write_begin() │ │ └── write_end() │ │ │ └── 块层 (磁盘文件系统) │ ├── 标记缓冲区脏 │ ├── 回写线程写回 │ └── submit_bio() │ ├── 【数据读取Pipeline】 │ │ │ ├── 用户层 │ │ └── read(fd, buf, len) │ │ │ ├── VFS层 │ │ └── vfs_read() → file->f_op->read_iter() │ │ │ ├── 文件系统层 (自定义) │ │ │ │ │ ├── myfs_read_iter() │ │ │ ├── 加读锁 (inode->i_rwsem) │ │ │ │ │ │ │ ├── 计算读取位置和大小 │ │ │ │ │ │ │ ├── 查找数据位置 │ │ │ │ └── myfs_get_block() → 逻辑块→物理块映射 │ │ │ │ │ │ │ ├── 读取数据 │ │ │ │ ├── 内存文件系统: memcpy到用户空间 │ │ │ │ ├── 页缓存命中: 直接返回 │ │ │ │ └── 页缓存未命中: 从磁盘读取 │ │ │ │ │ │ │ ├── 拷贝到用户空间 │ │ │ │ └── copy_to_user() │ │ │ │ │ │ │ └── 解锁 │ │ │ │ │ └── 更新访问时间 (atime) │ │ │ └── 块层 (磁盘文件系统) │ ├── 提交bio请求 │ ├── 磁盘DMA读取 │ └── 数据返回页缓存 │ ├── 【元数据操作Pipeline】 │ │ │ ├── 创建文件: myfs_create() │ │ ├── 分配inode │ │ ├── 初始化inode │ │ ├── 添加目录项 │ │ └── 更新父目录时间戳 │ │ │ ├── 删除文件: myfs_unlink() │ │ ├── 检查权限 │ │ ├── 从目录项删除 │ │ ├── 释放inode │ │ │ ├── 释放数据块 │ │ │ └── 标记inode空闲 │ │ └── 更新父目录 │ │ │ └── 重命名文件: myfs_rename() │ ├── 检查源和目标 │ ├── 更新目录项 │ └── 更新时间戳 │ └── 【缓存管理Pipeline】 │ ├── 页缓存 │ ├── 读缓存: find_get_page() │ ├── 写缓存: add_to_page_cache_lru() │ ├── 淘汰: page_cache_delete() │ └── 回写: writeback_single_inode() │ ├── dcache (目录项缓存) │ ├── 查找: d_lookup() │ ├── 添加: d_add() │ ├── 淘汰: shrink_dcache_parent() │ └── 回收: prune_dcache_sb() │ └── icache (inode缓存) ├── 查找: ilookup() ├── 添加: insert_inode_locked() ├── 淘汰: invalidate_inodes() └── 回收: prune_icache_sb()
总结与参考
核心要点总结
-
VFS是Linux文件系统的核心抽象层,通过超级块、索引节点、目录项、文件对象四大数据结构统一所有文件系统接口
-
系统调用路径: sys_open → path_openat → vfs_open → 文件系统特定操作;sys_read → vfs_read → generic_file_read_iter → address_space_operations
-
rootfs初始化是VFS建立的基础,通过init_mount_tree()创建根目录并挂载rootfs,为后续挂载其他文件系统提供挂载点
-
epoll事件分发基于文件poll操作,通过红黑树管理监视fd,就绪队列分发事件,回调机制实现主动通知
-
调试工具链从用户空间(df, iostat)到内核跟踪点(ftrace, trace-cmd),再到专用工具(debugfs, blktrace),形成完整调试体系
-
性能优化需从应用层、文件系统层、块层、硬件层分层分析,针对性调整参数和配置
关键源码文件索引
| 模块 | 路径 | 说明 |
|---|---|---|
| VFS核心 | fs/namei.c | 路径查找 |
| VFS核心 | fs/read_write.c | read/write系统调用 |
| VFS核心 | fs/super.c | 超级块管理 |
| VFS核心 | fs/dcache.c | 目录项缓存 |
| 页缓存 | mm/filemap.c | 页缓存核心 |
| EXT4 | fs/ext4/inode.c | inode操作 |
| EXT4 | fs/ext4/extents.c | extent树管理 |
| epoll | fs/eventpoll.c | epoll实现 |
| debugfs | fs/debugfs/ | 调试文件系统 |
第二阶段 由外至内视角:从mkdir到数据持久化的完整旅程
第一部分:实战场景 - 在Buildroot Linux中创建目录
1.1 场景重现:在终端执行mkdir
# 在Buildroot Linux终端执行 root@buildroot:~# mkdir /mnt/data/testdir root@buildroot:~# ls -la /mnt/data/ drwxr-xr-x 2 root root 4096 Mar 26 10:30 testdir
1.2 mkdir命令背后的完整调用链
mkdir /mnt/data/testdir 完整调用链
│
├── 【用户空间】BusyBox的mkdir命令
│ │
│ ├── 源代码位置: busybox/coreutils/mkdir.c
│ │
│ └── mkdir_main() 函数
│ ├── 1. 解析命令行参数
│ ├── 2. 循环处理每个目录路径
│ │ └── 对于 "/mnt/data/testdir"
│ │ ├── 调用 mkdir() 系统调用
│ │ │ └── syscall(__NR_mkdir, path, 0755)
│ │ │
│ │ └── 如果父目录不存在,根据-p选项决定是否递归创建
│ │
│ └── 3. 返回结果
│
├── 【系统调用入口】内核 fs/namei.c
│ │
│ ├── SYSCALL_DEFINE2(mkdir, const char __user *, pathname, umode_t, mode)
│ │ └── do_mkdirat(AT_FDCWD, pathname, mode)
│ │
│ └── do_mkdirat()
│ └── user_path_create() # 路径查找和创建准备
│ │
│ ├── filename_lookup() # 查找父目录
│ │ └── path_lookupat() # 路径解析核心
│ │ │
│ │ ├── walk_component() # 逐级解析
│ │ │ ├── "/" → 根目录dentry
│ │ │ ├── "mnt" → 查找mnt目录
│ │ │ └── "data" → 查找data目录
│ │ │
│ │ └── 返回父目录的路径结构
│ │
│ └── 准备创建新的dentry
│
├── 【VFS核心层】fs/namei.c
│ │
│ ├── vfs_mkdir() # VFS层mkdir
│ │ │
│ │ ├── 1. 权限检查
│ │ │ ├── 父目录可写检查
│ │ │ └── CAP_DAC_OVERRIDE能力检查
│ │ │
│ │ ├── 2. 调用具体文件系统的mkdir操作
│ │ │ └── dir->i_op->mkdir()
│ │ │
│ │ └── 3. 更新目录项缓存(dcache)
│ │
│ └── 实际文件系统调用 (以ext4为例)
│
├── 【EXT4文件系统层】fs/ext4/namei.c
│ │
│ ├── ext4_mkdir() # EXT4的mkdir实现
│ │ │
│ │ ├── 1. 开始日志事务
│ │ │ └── ext4_journal_start() # JBD2日志开始
│ │ │
│ │ ├── 2. 分配新的inode
│ │ │ └── ext4_new_inode_start_handle()
│ │ │ │
│ │ │ ├── ext4_claim_inode() # 分配inode号
│ │ │ │ └── 从inode位图找空闲位
│ │ │ │
│ │ │ ├── ext4_init_new_dir() # 初始化目录inode
│ │ │ │ ├── 设置i_mode = S_IFDIR | mode
│ │ │ │ ├── 设置i_uid, i_gid
│ │ │ │ ├── 设置时间戳 (atime, mtime, ctime)
│ │ │ │ └── 分配目录数据块
│ │ │ │ └── ext4_mb_new_blocks() # 多块分配器分配块
│ │ │ │
│ │ │ └── 初始化目录内容
│ │ │ └── 写入"."和".."条目
│ │ │
│ │ ├── 3. 将新目录项添加到父目录
│ │ │ └── ext4_add_entry()
│ │ │ │
│ │ │ ├── 在父目录的数据块中查找空闲位置
│ │ │ ├── 构建目录项结构 ext4_dir_entry_2
│ │ │ │ ├── inode = 新分配的inode号
│ │ │ │ ├── rec_len = 条目长度
│ │ │ │ ├── name_len = 8 ("testdir")
│ │ │ │ ├── file_type = EXT4_FT_DIR
│ │ │ │ └── name = "testdir"
│ │ │ └── 写入父目录的数据块
│ │ │
│ │ ├── 4. 更新父目录元数据
│ │ │ ├── 父目录inode的i_size增加
│ │ │ ├── 父目录inode的i_links_count增加
│ │ │ └── 父目录时间戳更新
│ │ │
│ │ ├── 5. 更新块组统计
│ │ │ ├── 更新块位图 (已用块数+1)
│ │ │ ├── 更新inode位图 (已用inode+1)
│ │ │ └── 更新组描述符
│ │ │
│ │ └── 6. 提交日志事务
│ │ └── ext4_journal_stop()
│ │
│ └── 关键数据结构变化
│ │
│ ├── 超级块变化
│ │ └── s_free_inodes_count-- (空闲inode数减1)
│ │
│ ├── 块组描述符变化
│ │ ├── bg_free_inodes_count--
│ │ └── bg_used_dirs_count++ (目录数+1)
│ │
│ └── 内存缓存变化
│ ├── dentry缓存: 添加新目录项
│ └── inode缓存: 添加新inode
│
└── 【块设备层】最终写入eMMC
│
├── 数据写入时机
│ ├── 日志提交时立即写入
│ └── 元数据写入回写机制
│
└── eMMC物理写入
└── eMMC控制器将数据写入NAND Flash
第二部分:实战场景 - touch文件创建
2.1 touch命令调用链
root@buildroot:~# touch /mnt/data/testdir/myfile.txt
touch文件创建完整调用链 │ ├── 【用户空间】BusyBox touch命令 │ │ │ ├── busybox/coreutils/touch.c │ │ │ └── touch_main() │ ├── 解析参数 │ ├── 对每个文件路径 │ │ └── 调用 utimensat() 系统调用 │ │ ├── 如果文件存在: 更新时间戳 │ │ └── 如果文件不存在: 创建空文件 │ │ │ └── 实际调用: open() + utimensat() │ ├── 【系统调用入口】 │ │ │ ├── SYSCALL_DEFINE4(open, ...) → do_sys_open() │ │ └── do_filp_open() # 打开或创建文件 │ │ │ └── vfs_open() → ext4_file_open() (已存在) 或 ext4_create() (新建) │ ├── 【EXT4文件创建】fs/ext4/namei.c │ │ │ └── ext4_create() # 创建普通文件 │ │ │ ├── 1. 开始日志事务 │ │ └── ext4_journal_start() │ │ │ ├── 2. 分配inode │ │ └── ext4_new_inode_start_handle() │ │ │ │ │ ├── 从inode位图分配空闲inode │ │ │ └── 例如分配inode号 = 12345 │ │ │ │ │ └── 初始化inode │ │ ├── i_mode = S_IFREG | 0644 (普通文件) │ │ ├── i_size = 0 (空文件) │ │ ├── i_blocks = 0 (无数据块) │ │ ├── i_atime/mtime/ctime = 当前时间 │ │ └── i_links_count = 1 │ │ │ ├── 3. 添加到父目录 │ │ └── ext4_add_entry() │ │ └── 在父目录(/mnt/data/testdir)的目录块中添加 │ │ └── ext4_dir_entry: name="myfile.txt", inode=12345 │ │ │ ├── 4. 更新父目录 │ │ ├── 父目录i_size增加 (如果新条目导致扩展) │ │ └── 父目录mtime/ctime更新 │ │ │ ├── 5. 更新统计 │ │ ├── 超级块: s_free_inodes_count-- │ │ └── 块组描述符: bg_free_inodes_count-- │ │ │ └── 6. 提交日志 │ └── ext4_journal_stop() │ └── 【文件打开】ext4_file_open() │ └── 对于空文件,无需分配数据块 └── 文件的数据块为0,直到第一次写入才分配
2.2 写入数据到文件的流程
root@buildroot:~# echo "Hello World" > /mnt/data/testdir/myfile.txt
写入数据完整调用链
│
├── 【用户空间】shell重定向
│ │
│ ├── echo命令输出到stdout
│ └── shell将stdout重定向到文件
│ └── open("myfile.txt", O_WRONLY|O_CREAT|O_TRUNC)
│ └── write(fd, "Hello World\n", 12)
│
├── 【VFS层】fs/read_write.c
│ │
│ └── vfs_write() → file->f_op->write_iter()
│
├── 【EXT4写操作】fs/ext4/file.c
│ │
│ └── ext4_file_write_iter()
│ │
│ ├── 检查文件大小 (之前是0)
│ │
│ ├── ext4_buffered_write_iter() (缓冲写)
│ │ │
│ │ ├── generic_perform_write()
│ │ │ │
│ │ │ └── 第一次写入时分配数据块
│ │ │ │
│ │ │ ├── ext4_da_write_begin() # 延迟分配写开始
│ │ │ │ │
│ │ │ │ ├── ext4_map_blocks() # 分配新块
│ │ │ │ │ │
│ │ │ │ │ ├── 查找extent树 (空)
│ │ │ │ │ ├── ext4_ext_map_blocks()
│ │ │ │ │ └── ext4_mb_new_blocks() # 多块分配器
│ │ │ │ │ │
│ │ │ │ │ ├── 选择最佳块组
│ │ │ │ │ ├── 分配物理块: 例如块号 102400
│ │ │ │ │ ├── 更新块位图
│ │ │ │ │ └── 更新组描述符
│ │ │ │ │
│ │ │ │ └── 创建extent项
│ │ │ │ └── extent: ee_block=0, ee_len=1, ee_start=102400
│ │ │ │
│ │ │ └── ext4_da_write_end()
│ │ │ ├── 拷贝数据到页缓存
│ │ │ ├── 标记页为脏 (PG_dirty)
│ │ │ └── 更新inode大小: i_size = 12
│ │ │
│ │ └── 更新inode时间戳
│ │ └── i_mtime = i_ctime = 当前时间
│ │
│ └── 返回写入字节数
│
├── 【页缓存】mm/filemap.c
│ │
│ ├── 数据存储在内存页中
│ │ └── struct page (4KB页) 包含 "Hello World\n"
│ │
│ └── 脏页标记,等待回写
│
└── 【回写机制】异步写入eMMC
│
└── 当满足以下条件时触发回写:
├── 脏页比例超过阈值 (dirty_ratio = 20%)
├── 定时器触发 (dirty_expire_centisecs = 3000)
└── sync命令或系统关机时
2.3 写入后的eMMC物理布局
eMMC物理布局变化 │ ├── 创建目录 /mnt/data/testdir 后的布局 │ │ │ ├── 块组描述符 │ │ ├── bg_free_inodes_count: 减少1 │ │ └── bg_used_dirs_count: 增加1 │ │ │ ├── Inode位图 │ │ └── 某一位从0变1 (inode号被占用) │ │ │ ├── Inode表 │ │ ├── inode 12344: 目录 /mnt/data 的inode │ │ └── inode 12345: 新目录 testdir 的inode │ │ ├── i_mode = 0x41ED (目录) │ │ ├── i_size = 4KB (一个块) │ │ └── i_block[0] = 102400 (数据块) │ │ │ └── 数据块 │ └── 块 102400: 目录内容 │ ├── ".", inode=12345 │ └── "..", inode=12344 │ └── 创建文件 myfile.txt 并写入后的布局 │ ├── Inode表 │ ├── inode 12346: 新文件 myfile.txt │ │ ├── i_mode = 0x81A4 (普通文件, 0644) │ │ ├── i_size = 12 │ │ ├── i_blocks = 8 (512字节块数,实际1个4KB块) │ │ ├── i_block[0] = 102401 (数据块) │ │ └── i_extent树: extent(0, 1, 102401) │ │ │ └── 父目录 inode 12345 │ ├── i_size从4KB增加到8KB (如果目录扩展) │ └── 目录数据块增加新条目 │ ├── 数据块 │ └── 块 102401: 文件内容 │ └── "Hello World\n\0\0..." (填充到4KB) │ └── 块位图 └── 块 102401 被标记为已用
第三部分:开机后从rootfs读取文件
3.1 系统启动流程与文件系统挂载
Buildroot Linux启动与文件系统加载
│
├── 【Bootloader阶段】U-Boot
│ │
│ ├── 从eMMC读取内核和设备树
│ │ ├── mmc read 0x48000000 0x800 0x8000
│ │ └── bootz 0x48000000 - 0x49000000
│ │
│ └── 传递内核参数
│ └── root=/dev/mmcblk0p2 rootfstype=ext4
│
├── 【内核初始化】init/main.c
│ │
│ ├── start_kernel()
│ │ ├── vfs_caches_init() # 初始化VFS缓存
│ │ │ ├── dcache_init() # 目录项缓存
│ │ │ ├── inode_init() # inode缓存
│ │ │ └── files_init() # 文件表
│ │ │
│ │ └── kernel_init() # 内核线程
│ │
│ └── init_post() # 启动init进程
│
├── 【根文件系统挂载】init/do_mounts.c
│ │
│ ├── prepare_namespace()
│ │ │
│ │ ├── 解析root=参数: /dev/mmcblk0p2
│ │ │
│ │ ├── mount_block_root() # 挂载块设备根文件系统
│ │ │ │
│ │ │ ├── do_mount_root()
│ │ │ │ ├── sys_mount(dev_name, "/root", fs_type, flags, data)
│ │ │ │ │ └── 调用文件系统的mount函数
│ │ │ │ └── sys_chdir("/root")
│ │ │ │
│ │ │ └── 将rootfs切换到真正的根文件系统
│ │ │ └── sys_pivot_root("/root", "/root/old")
│ │ │
│ │ └── 初始化devtmpfs挂载 /dev
│ │
│ └── 关键: 此时内核从eMMC读取超级块
│ └── ext4_fill_super()
│ ├── 读取超级块 (eMMC块0)
│ ├── 验证s_magic = 0xEF53
│ ├── 读取组描述符
│ └── 创建根目录dentry和inode
│
├── 【用户空间init】BusyBox init
│ │
│ ├── /sbin/init (指向busybox)
│ │ ├── 解析 /etc/inittab
│ │ ├── 挂载 proc, sysfs, tmpfs
│ │ └── 启动shell (/bin/sh)
│ │
│ └── 用户获得shell提示符
│
└── 【读取文件】ls /mnt/data/testdir/myfile.txt
│
└── 当用户执行命令时
│
├── 路径解析过程
│ ├── "/" → 根目录dentry (已缓存在dcache)
│ ├── "mnt" → 查找dentry
│ │ └── 从根目录inode读取目录数据块
│ │ └── 从eMMC读取目录块 (可能已缓存)
│ ├── "data" → 查找子目录
│ │ └── 从mnt的inode读取目录块
│ └── "testdir" → 查找子目录
│ └── 从data的inode读取目录块
│
├── 最终找到文件inode (12346)
│ ├── 从inode表读取inode信息
│ │ └── 从eMMC读取inode块
│ │ └── 读取到 i_block[0]=102401
│ │
│ └── 读取文件内容
│ └── 从数据块102401读取"Hello World\n"
│
└── 显示文件信息
3.2 页面缓存的作用 - 加速后续读取
页面缓存工作原理 │ ├── 【第一次读取】读取myfile.txt │ │ │ ├── 1. VFS调用 generic_file_read_iter() │ │ │ ├── 2. 查找页面缓存 │ │ └── find_get_page(mapping, index=0) │ │ └── 返回 NULL (未命中) │ │ │ ├── 3. 分配新页面 │ │ └── page_cache_alloc() │ │ │ ├── 4. 从磁盘读取数据到页面 │ │ ├── submit_bio() → eMMC读取 │ │ ├── eMMC DMA传输到页面内存 │ │ └── 等待I/O完成 │ │ │ ├── 5. 将页面加入页面缓存 │ │ └── add_to_page_cache_lru(page, mapping, index) │ │ │ └── 6. 拷贝数据到用户空间 │ ├── 【第二次读取】再次读取同一个文件 │ │ │ ├── 1. 查找页面缓存 │ │ └── find_get_page(mapping, index=0) │ │ └── 返回缓存的页面 (命中!) │ │ │ └── 2. 直接从内存拷贝到用户空间 │ └── 无需访问eMMC,速度极快 │ └── 【页面淘汰】内存不足时 │ ├── LRU链表管理 │ └── 最近最少使用的页面被淘汰 │ └── 脏页回写后释放
3.3 dcache和icache的作用
dcache和icache加速路径查找 │ ├── 【第一次查找】ls /mnt/data/testdir │ │ │ ├── 1. 查找根目录dentry │ │ └── d_lookup(root, "mnt") │ │ ├── 在dcache中查找 │ │ └── 未命中 → 从磁盘读取 │ │ │ ├── 2. 查找mnt的dentry │ │ ├── d_lookup(mnt_dentry, "data") │ │ └── 未命中 → 从磁盘读取 │ │ │ ├── 3. 查找data的dentry │ │ ├── d_lookup(data_dentry, "testdir") │ │ └── 未命中 → 从磁盘读取 │ │ │ └── 所有dentry被缓存 │ ├── 【第二次查找】再次执行ls │ │ │ ├── d_lookup(root, "mnt") → 缓存命中 │ ├── d_lookup(mnt_dentry, "data") → 缓存命中 │ ├── d_lookup(data_dentry, "testdir") → 缓存命中 │ │ │ └── 完全无需访问eMMC,性能提升巨大 │ └── icache类似,缓存inode信息
第四部分:eMMC存储管理综合架构
4.1 eMMC物理结构与分区布局
eMMC存储布局 (以16GB eMMC为例) │ ├── 【硬件结构】 │ │ │ ├── eMMC内部架构 │ │ ├── MMC控制器 │ │ ├── NAND Flash芯片 (多个Die) │ │ │ ├── SLC区域 (高速,寿命长) │ │ │ ├── MLC/TLC区域 (容量大) │ │ │ └── 备用块 (替换坏块) │ │ └── 嵌入式固件 (FTL层) │ │ ├── 坏块管理 │ │ ├── 磨损均衡 (Wear Leveling) │ │ ├── 垃圾回收 (GC) │ │ └── 映射表 (逻辑块到物理块) │ │ │ └── 主机看到的设备: /dev/mmcblk0 │ ├── 【分区布局】通过U-Boot或Linux分区工具 │ │ │ ├── /dev/mmcblk0p1: boot分区 (64MB) │ │ └── U-Boot, kernel, dtb │ │ │ ├── /dev/mmcblk0p2: rootfs分区 (4GB) │ │ └── ext4文件系统,只读或读写 │ │ │ ├── /dev/mmcblk0p3: data分区 (8GB) │ │ └── ext4文件系统,用户数据 │ │ │ └── /dev/mmcblk0p4: swap分区 (512MB) │ └── swap空间 │ └── 【分区表】GPT或MBR └── 存储在eMMC开头扇区
4.2 块设备层到eMMC驱动的完整路径
从文件系统到eMMC物理写入的完整路径
│
├── 【文件系统层】ext4
│ │
│ ├── ext4_writepages() # 回写脏页
│ │ └── mpage_writepages() # 多页回写
│ │ └── submit_bio() # 提交块I/O请求
│ │
│ └── BIO结构 (block I/O)
│ struct bio {
│ struct bio *bi_next;
│ struct block_device *bi_bdev; # /dev/mmcblk0p2
│ unsigned int bi_opf; # REQ_OP_WRITE
│ struct bio_vec bi_io_vec[]; # 数据段
│ bio_end_io_t *bi_end_io; # 完成回调
│ };
│
├── 【通用块层】block/blk-core.c
│ │
│ ├── submit_bio() # BIO提交入口
│ │ ├── blk_mq_submit_bio() # 多队列块层
│ │ │ ├── 选择硬件队列
│ │ │ ├── 分配请求结构
│ │ │ │ └── struct request
│ │ │ └── 插入调度器
│ │ └── 调用队列的request_fn
│ │
│ └── I/O调度器 (可选)
│ ├── mq-deadline (默认)
│ │ └── 合并相邻请求,排序
│ └── none (绕过调度器)
│
├── 【MMC块设备层】drivers/mmc/core/block.c
│ │
│ ├── mmc_blk_issue_rq() # MMC请求处理
│ │ │
│ │ ├── 构建MMC命令
│ │ │ ├── CMD25 (多块写)
│ │ │ ├── CMD18 (多块读)
│ │ │ └── CMD24 (单块写)
│ │ │
│ │ ├── 设置块地址 (LBA)
│ │ │ └── 逻辑块地址 → eMMC内部逻辑地址
│ │ │
│ │ └── mmc_blk_rw_cmd()
│ │
│ └── 数据准备
│ ├── 构建MMC数据包
│ ├── 设置DMA传输
│ └── 发送命令到eMMC
│
├── 【MMC核心层】drivers/mmc/core/core.c
│ │
│ ├── mmc_start_request() # 启动MMC请求
│ │ │
│ │ ├── 检查总线状态
│ │ ├── 发送CMD (通过主机控制器)
│ │ └── 启动DMA传输
│ │ │
│ │ └── mmc_wait_for_req() # 等待完成
│ │ └── 中断处理函数唤醒
│ │
│ └── mmc_request_done() # 请求完成回调
│ ├── 通知块层完成
│ └── 释放资源
│
├── 【MMC主机控制器驱动】drivers/mmc/host/
│ │
│ ├── dw_mmc.c (DesignWare MMC)
│ │ ├── dw_mci_request() # 发送请求
│ │ ├── 设置描述符表
│ │ ├── 写入命令寄存器
│ │ └── 启动传输
│ │
│ └── 硬件操作
│ ├── 配置CLK (时钟频率)
│ ├── 配置总线宽度 (1/4/8-bit)
│ ├── 发送命令
│ └── DMA传输数据
│
└── 【物理层】eMMC芯片
│
├── 接收CMD和DATA信号
│ ├── 命令线 (CMD)
│ ├── 数据线 (DAT0-DAT7)
│ └── 时钟线 (CLK)
│
├── eMMC内部处理
│ ├── FTL映射: LBA → 物理块
│ ├── 坏块检查
│ ├── 磨损均衡选择物理块
│ └── 垃圾回收触发
│
└── 写入NAND Flash
├── 页编程 (Page Program)
└── 块擦除 (Block Erase) (如需要)
4.3 多设备文件系统统一管理
Linux多设备文件系统架构
│
├── 【设备文件】/dev目录
│ │
│ ├── /dev/mmcblk0 # eMMC块设备
│ │ ├── /dev/mmcblk0p1 (boot)
│ │ ├── /dev/mmcblk0p2 (rootfs)
│ │ └── /dev/mmcblk0p3 (data)
│ │
│ ├── /dev/sda # USB硬盘
│ │ ├── /dev/sda1 (FAT32)
│ │ └── /dev/sda2 (NTFS)
│ │
│ ├── /dev/mmcblk1 # SD卡
│ │ └── /dev/mmcblk1p1 (exFAT)
│ │
│ └── /dev/mtdblock0 # SPI Flash
│ └── /dev/mtdblock0 (jffs2)
│
├── 【挂载点】/mnt和/media
│ │
│ ├── 根文件系统: / (ext4 on /dev/mmcblk0p2)
│ │
│ ├── /mnt/data: ext4 on /dev/mmcblk0p3
│ │ └── 用户数据分区
│ │
│ ├── /mnt/usb: vfat on /dev/sda1
│ │ └── 自动挂载的U盘
│ │
│ ├── /mnt/sdcard: exfat on /dev/mmcblk1p1
│ │ └── SD卡
│ │
│ └── /mnt/spi: jffs2 on /dev/mtdblock0
│ └── 配置存储
│
├── 【统一操作接口】VFS层
│ │
│ └── 无论底层是什么设备,用户看到统一接口
│ ├── open("/mnt/data/file.txt")
│ ├── open("/mnt/usb/file.txt")
│ ├── open("/mnt/sdcard/file.txt")
│ └── 都是通过相同的系统调用
│
└── 【块设备操作差异】
│
├── eMMC: mmc_blk_ops
│ └── 调用dw_mmc驱动
│
├── USB硬盘: usb_storage_ops
│ └── 调用USB mass storage驱动
│
└── SD卡: mmc_blk_ops
└── 调用SDHCI驱动
第五部分:实战 - USB硬盘和SD卡操作流程
5.1 插入USB硬盘的完整流程
USB硬盘热插拔流程 │ ├── 【硬件层】USB接口检测 │ │ │ ├── USB控制器检测设备插入 │ ├── 总线枚举 │ └── 分配USB地址 │ ├── 【USB核心层】drivers/usb/core/ │ │ │ ├── usb_new_device() │ │ ├── 读取设备描述符 │ │ ├── 分配配置 │ │ └── 匹配驱动 │ │ │ └── usb_stor_probe() # USB存储驱动 │ ├── 初始化SCSI层 │ └── scsi_add_host() │ ├── 【SCSI层】drivers/scsi/ │ │ │ ├── scsi_scan_host() │ │ ├── 发送INQUIRY命令 │ │ ├── 读取容量: READ CAPACITY │ │ └── 创建sd设备: /dev/sda │ │ │ └── sd_probe() # SCSI磁盘驱动 │ ├── 读取分区表 │ ├── 创建分区: /dev/sda1, /dev/sda2 │ └── 通知用户空间: uevent │ ├── 【用户空间】udev/mdev │ │ │ ├── 接收uevent事件 │ │ └── ACTION=add, DEVPATH=/devices/... │ │ │ ├── 执行挂载脚本 │ │ └── /etc/udev/rules.d/automount.rules │ │ └── ACTION=="add", RUN+="/bin/mount /dev/sda1 /mnt/usb" │ │ │ └── 挂载文件系统 │ ├── 探测文件系统类型 (blkid) │ ├── mount -t vfat /dev/sda1 /mnt/usb │ └── 通知用户 │ └── 【用户操作】 └── ls /mnt/usb/ # 访问USB硬盘内容
5.2 从USB硬盘读取文件的调用链
从USB硬盘读取文件的完整调用链 │ ├── 【用户命令】 │ └── cat /mnt/usb/movie.mp4 │ ├── 【VFS层】 │ └── vfs_read() → file->f_op->read_iter() │ ├── 【具体文件系统】FAT32 (vfat) │ │ │ ├── fat_file_read_iter() # fs/fat/file.c │ │ │ │ │ ├── 查找文件簇链 │ │ │ └── fat_get_cluster() # FAT表查找 │ │ │ │ │ └── 读取数据 │ │ └── fat_readpages() │ │ └── mpage_readpages() │ │ └── submit_bio() │ │ │ └── 关键: FAT32的元数据组织 │ ├── 引导扇区 (MBR) │ ├── FAT表 (文件分配表) │ ├── 根目录 │ └── 数据区 │ ├── 【块层】提交到USB硬盘 │ │ │ ├── 构建BIO,目标设备 /dev/sda1 │ │ │ ├── 通用块层处理 │ │ │ └── SCSI层转换 │ └── sd_init_command() │ ├── 构建SCSI命令: READ(10) │ ├── 设置LBA地址 │ └── 设置传输长度 │ ├── 【USB存储驱动】 │ │ │ ├── usb_stor_control_thread() │ │ ├── 封装为USB Bulk-Only传输 │ │ └── 发送URB (USB Request Block) │ │ │ └── USB核心层 │ └── usb_submit_urb() │ ├── 【USB主机控制器】EHCI/xHCI │ │ │ ├── 构建USB事务 │ ├── 通过USB总线发送 │ └── USB硬盘响应 │ └── 【数据返回路径】 ├── USB中断 ├── 数据DMA到内存 ├── URB完成回调 ├── SCSI完成 ├── BIO完成 └── 数据返回用户空间
5.3 不同文件系统的操作差异对比
文件系统操作对比表
│
├── 【创建文件】open("new.txt", O_CREAT)
│ │
│ ├── ext4 (eMMC)
│ │ ├── 分配inode
│ │ ├── 写入inode表
│ │ ├── 更新目录项
│ │ ├── 更新块位图
│ │ ├── 日志记录 (JBD2)
│ │ └── 最终写入eMMC
│ │
│ ├── FAT32 (USB硬盘)
│ │ ├── 查找空闲簇 (FAT表)
│ │ ├── 分配目录项 (32字节)
│ │ ├── 更新FAT表
│ │ ├── 写入目录项
│ │ └── 直接写入 (无日志)
│ │
│ ├── NTFS (Windows硬盘)
│ │ ├── 分配MFT记录
│ │ ├── 更新$MFT文件
│ │ ├── 更新目录索引
│ │ ├── 日志记录 ($LogFile)
│ │ └── 写入块设备
│ │
│ └── jffs2 (SPI Flash)
│ ├── 遍历擦除块
│ ├── 分配空闲节点
│ ├── 写入压缩数据
│ ├── 更新inode节点
│ └── 垃圾回收清理旧节点
│
├── 【写入数据】write(fd, data, size)
│ │
│ ├── ext4
│ │ ├── 页缓存
│ │ ├── 延迟分配
│ │ ├── 回写机制
│ │ └── 日志提交
│ │
│ ├── FAT32
│ │ ├── 页缓存
│ │ ├── 直接分配簇
│ │ ├── 立即更新FAT表
│ │ └── 同步写入 (通常)
│ │
│ ├── NTFS
│ │ ├── 日志记录
│ │ ├── 属性更新
│ │ └── 延迟写入
│ │
│ └── jffs2
│ ├── 直接写入Flash
│ ├── 节点追加
│ ├── 原地更新 (新节点)
│ └── 垃圾回收标记
│
└── 【读取文件】read(fd, buf, size)
│
├── ext4
│ ├── 页缓存查找
│ ├── extent树定位
│ └── 块映射读取
│
├── FAT32
│ ├── 页缓存查找
│ ├── FAT链遍历
│ └── 簇映射读取
│
├── NTFS
│ ├── 页缓存查找
│ ├── MFT属性读取
│ ├── extent列表定位
│ └── 簇映射读取
│
└── jffs2
├── 读取节点链表
├── 解压缩 (如果压缩)
└── 页缓存 (可选)
第六部分:调试与监控实战
6.1 实时监控文件系统操作
# 1. 使用strace跟踪mkdir命令
root@buildroot:~# strace -e trace=file mkdir /mnt/data/testdir
execve("/bin/mkdir", ["mkdir", "/mnt/data/testdir"], 0xbea6d830 /* 12 vars */) = 0
...
mkdir("/mnt/data/testdir", 0755) = 0
+++ exited with 0 +++
# 2. 使用inotify监控目录变化
root@buildroot:~# inotifywait -m -r /mnt/data/
Setting up watches. Beware: since -r was given, this may take a while!
Watches established.
/mnt/data/ CREATE testdir
/mnt/data/ OPEN testdir
/mnt/data/ CLOSE_NOWRITE_CLOSE testdir
# 3. 使用ftrace追踪内核函数
root@buildroot:~# echo ext4_mkdir > /sys/kernel/tracing/set_ftrace_filter
root@buildroot:~# echo function > /sys/kernel/tracing/current_tracer
root@buildroot:~# mkdir /mnt/data/testdir
root@buildroot:~# cat /sys/kernel/tracing/trace
# tracer: function
#
# TASK-PID CPU# TIMESTAMP FUNCTION
# | | | |
mkdir-1234 [001] 123.456789: ext4_mkdir <-vfs_mkdir
mkdir-1234 [001] 123.456790: ext4_new_inode_start_handle <-ext4_mkdir
mkdir-1234 [001] 123.456791: ext4_mb_new_blocks <-ext4_new_inode_start_handle
...
# 4. 使用blktrace追踪eMMC写入
root@buildroot:~# blktrace -d /dev/mmcblk0 -o trace
^C
root@buildroot:~# blkparse -i trace > trace.txt
root@buildroot:~# head trace.txt
8,0 1 1 0.000000000 1234 A WS 102400 + 8 <- (mmcblk0p2)
8,0 1 2 0.000001000 1234 Q WS 102400 + 8 [mkdir]
8,0 1 3 0.000002000 1234 G WS 102400 + 8 [mkdir]
8,0 1 4 0.000003000 1234 I WS 102400 + 8 [mkdir]
8,0 1 5 0.000004000 1234 D WS 102400 + 8 [mkdir]
6.2 查看文件系统状态
# 查看挂载信息 root@buildroot:~# mount /dev/mmcblk0p2 on / type ext4 (rw,relatime) devtmpfs on /dev type devtmpfs (rw,relatime,size=1024k) proc on /proc type proc (rw,relatime) sysfs on /sys type sysfs (rw,relatime) /dev/mmcblk0p3 on /mnt/data type ext4 (rw,relatime) /dev/sda1 on /mnt/usb type vfat (rw,relatime) # 查看磁盘使用情况 root@buildroot:~# df -h Filesystem Size Used Avail Use% Mounted on /dev/mmcblk0p2 3.9G 500M 3.2G 14% / /dev/mmcblk0p3 7.8G 100M 7.3G 2% /mnt/data /dev/sda1 28G 15G 13G 54% /mnt/usb # 查看inode使用情况 root@buildroot:~# df -i Filesystem Inodes IUsed IFree IUse% Mounted on /dev/mmcblk0p2 262144 1234 260910 1% / /dev/mmcblk0p3 524288 56 524232 1% /mnt/data /dev/sda1 0 0 0 - /mnt/usb # FAT32无inode概念 # 查看文件系统详细信息 root@buildroot:~# tune2fs -l /dev/mmcblk0p2 Filesystem volume name: rootfs Last mounted on: / Filesystem UUID: a1b2c3d4-... Filesystem magic number: 0xEF53 Filesystem revision #: 1 (dynamic) Filesystem features: has_journal ext_attr resize_inode dir_index ... Inode count: 262144 Block count: 1048576 Reserved block count: 52428 Free blocks: 820000 Free inodes: 260910
6.3 性能分析实战
# 1. 使用iostat监控I/O性能 root@buildroot:~# iostat -x 1 avg-cpu: %user %nice %system %iowait %steal %idle 2.50 0.00 5.00 10.00 0.00 82.50 Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await r_await w_await svctm %util mmcblk0 0.00 5.00 0.00 10.00 0.00 200.00 40.00 0.50 50.00 0.00 50.00 10.00 10.00 sda 0.00 0.00 50.00 0.00 1024.00 0.00 40.00 0.10 2.00 2.00 0.00 1.00 5.00 # 2. 使用perf分析文件系统热点 root@buildroot:~# perf top -e ext4:ext4_writepages Samples: 5K of event 'ext4:ext4_writepages', Event count (approx.): 5000 Children Self Shared Object Symbol + 50.00% 50.00% [kernel] [k] ext4_writepages + 30.00% 30.00% [kernel] [k] ext4_map_blocks + 15.00% 15.00% [kernel] [k] ext4_mb_new_blocks + 5.00% 5.00% [kernel] [k] jbd2_journal_start # 3. 使用echo向eMMC写入测试 root@buildroot:~# time dd if=/dev/zero of=/mnt/data/testfile bs=1M count=100 100+0 records in 100+0 records out 104857600 bytes (100.0MB) copied, 2.34 seconds, 42.8 MB/s real 0m 2.34s user 0m 0.00s sys 0m 0.56s
总结
-
mkdir到eMMC的完整路径: 用户命令 → 系统调用 → VFS → ext4 → 块层 → MMC驱动 → eMMC硬件
-
touch文件创建: 分配inode → 添加目录项 → 更新位图 → 日志记录
-
开机后读取: 内核挂载rootfs → 读取超级块 → 构建VFS缓存 → 用户访问时从eMMC读取
-
多设备统一管理: VFS抽象层统一接口,底层驱动处理不同硬件差异
-
USB/SD卡: 热插拔检测 → 设备创建 → 自动挂载 → 通过相同VFS接口访问
-
调试工具链: strace(用户层) → ftrace(内核函数) → blktrace(块层) → 完整追踪数据流
关键理解: Linux文件系统的精髓在于VFS层的抽象,让用户无论操作eMMC、USB硬盘还是SD卡,都使用相同的open/read/write接口,而底层驱动负责与具体硬件通信。
第三阶段 Linux文件系统只读问题深度解析:从现象到根因
问题场景重现
# 典型问题现象
root@buildroot:~# touch /mnt/data/test.txt
touch: /mnt/data/test.txt: Read-only file system
# 或是在应用程序中
FILE *fp = fopen("/mnt/data/config.ini", "w");
if (fp == NULL) {
perror("fopen"); // 输出: fopen: Read-only file system
}
一、open与fopen的本质区别
1.1 系统调用vs库函数
open() 与 fopen() 对比 │ ├── 【open()】系统调用 │ │ │ ├── 原型: int open(const char *pathname, int flags, mode_t mode) │ │ │ ├── 返回: 文件描述符 (整数) │ │ │ ├── 执行路径: │ │ open() → 系统调用 → SYSCALL_DEFINE3(open) → do_sys_open() │ │ → do_filp_open() → vfs_open() → 具体文件系统open │ │ │ ├── 错误返回: -1, errno设置 │ │ │ └── 特点: 直接进入内核,无缓冲 │ └── 【fopen()】标准C库函数 │ ├── 原型: FILE *fopen(const char *pathname, const char *mode) │ ├── 返回: FILE结构体指针 │ ├── 执行路径: │ fopen() → 用户空间库函数 → open()系统调用 → ... → 返回FILE* │ ├── 特点: │ ├── 在open()基础上增加缓冲区 (stdio缓冲) │ ├── 可能受限于glibc的权限检查 │ └── 同一个文件描述符,错误码相同 │ └── 核心: fopen最终调用open(),只读问题根因相同
二、文件变成只读的五大根因
2.1 根因一:文件系统损坏(最致命)
文件系统损坏导致只读 │ ├── 【现象】 │ │ │ ├── 内核日志: EXT4-fs error (device mmcblk0p2): │ │ ext4_find_entry: 1234: inode #5678: comm bash: │ │ reading directory lblock 0 │ │ │ └── 系统自动将文件系统重新挂载为只读 │ ├── 【触发条件】 │ │ │ ├── 异常断电 │ │ └── 正在写入时掉电,元数据不一致 │ │ │ ├── 硬件故障 │ │ ├── eMMC坏块 │ │ ├── 存储芯片老化 │ │ └── 接触不良 (SD卡) │ │ │ ├── 内核BUG │ │ └── 文件系统驱动异常 │ │ │ └── 不正常的卸载 │ └── 未sync直接拔卡 │ ├── 【内核自动保护机制】 │ │ │ ├── 检测到文件系统错误 │ │ │ │ │ └── 内核函数: ext4_handle_error() │ │ │ │ │ ├── 打印错误日志 │ │ ├── 设置 SB_RDONLY 标志 │ │ └── 调用 remount_ro() 重新挂载为只读 │ │ │ └── 代码路径: │ fs/ext4/super.c: ext4_handle_error() │ ├── if (sb_rdonly(sb)) return; │ ├── printk(KERN_CRIT "EXT4-fs error ..."); │ ├── sb->s_flags |= SB_RDONLY; │ └── sync_filesystem(sb); │ └── 【诊断方法】 │ ├── dmesg | grep -i "ext4.*error" │ └── [12345.678] EXT4-fs error (device mmcblk0p2): │ ext4_find_entry:1452: inode #2: comm ls: reading directory lblock 0 │ ├── mount | grep ro │ └── /dev/mmcblk0p2 on / type ext4 (ro,relatime) │ └── fsck修复 └── umount /mnt/data && fsck.ext4 -y /dev/mmcblk0p2
2.2 根因二:挂载选项错误
挂载选项导致的只读 │ ├── 【主动挂载为只读】 │ │ │ ├── mount -o remount,ro /mnt/data │ │ └── 管理员主动设置只读 │ │ │ ├── fstab配置错误 │ │ └── /etc/fstab: /dev/mmcblk0p3 /mnt/data ext4 defaults,ro 0 2 │ │ │ └── 内核启动参数 │ └── root=/dev/mmcblk0p2 ro │ ├── 【被动只读】 │ │ │ ├── 设备写入错误 │ │ │ │ │ ├── eMMC/SD卡写保护开关 (SD卡物理开关) │ │ │ └── 卡座上的写保护检测引脚 │ │ │ │ │ ├── 设备返回写保护错误 │ │ │ └── MMC命令响应: WP_VIOLATION │ │ │ │ │ └── 代码路径: │ │ drivers/mmc/core/block.c: mmc_blk_issue_rq() │ │ ├── if (card->read_only) │ │ │ └── return -EROFS │ │ └── 块层返回错误 │ │ │ └── 文件系统检测到挂载时错误 │ └── mount时检测到需要fsck,自动只读挂载 │ ├── 【诊断方法】 │ │ │ ├── 查看挂载选项 │ │ └── cat /proc/mounts | grep /mnt/data │ │ │ ├── 检查fstab │ │ └── grep /mnt/data /etc/fstab │ │ │ └── 检查设备写保护 │ └── cat /sys/block/mmcblk0/ro │ └── 0=可写, 1=只读 │ └── 【修复方法】 └── mount -o remount,rw /mnt/data
2.3 根因三:权限问题(最常见)
权限不足导致的"只读"错误 │ ├── 【文件权限】 │ │ │ ├── 用户没有写权限 │ │ │ │ │ ├── 文件所有者不是当前用户 │ │ │ └── ls -l /mnt/data/config.ini │ │ │ -rw-r--r-- 1 root root 1024 Mar 26 10:00 config.ini │ │ │ # 普通用户无写权限 │ │ │ │ │ └── 目录没有写权限 │ │ └── ls -ld /mnt/data/ │ │ drwxr-xr-x 2 root root 4096 Mar 26 10:00 /mnt/data/ │ │ # 普通用户无法创建文件 │ │ │ ├── 文件系统挂载时限制了权限 │ │ └── mount -o uid=1000,gid=1000,umask=022 ... │ │ │ └── 代码路径: │ fs/namei.c: may_open() │ ├── if (!(file->f_mode & FMODE_WRITE)) │ │ └── return -EACCES │ └── 检查inode权限 │ ├── 【文件属性】 │ │ │ ├── 不可变属性 (immutable) │ │ └── chattr +i /mnt/data/config.ini │ │ └── lsattr config.ini: ----i--------e---- │ │ │ └── 只追加属性 (append-only) │ └── chattr +a /mnt/data/log.txt │ └── 只能追加,不能修改或删除 │ ├── 【诊断方法】 │ │ │ ├── 检查用户身份 │ │ └── id │ │ uid=1000(user) gid=1000(user) │ │ │ ├── 检查文件/目录权限 │ │ ├── ls -l /mnt/data/config.ini │ │ └── ls -ld /mnt/data/ │ │ │ ├── 检查扩展属性 │ │ └── lsattr /mnt/data/config.ini │ │ │ └── 尝试以root身份测试 │ └── sudo touch /mnt/data/test.txt │ └── 【修复方法】 ├── 修改权限: chmod 666 /mnt/data/config.ini ├── 修改所有者: chown user:user /mnt/data/ ├── 移除不可变: chattr -i /mnt/data/config.ini └── 使用sudo
2.4 根因四:空间不足
磁盘空间不足导致写入失败 │ ├── 【现象】 │ │ │ ├── open() 返回 -1, errno = ENOSPC │ │ │ ├── fopen() 返回 NULL, errno = ENOSPC │ │ │ └── 错误信息: No space left on device │ ├── 【两种空间耗尽】 │ │ │ ├── 数据块耗尽 │ │ ├── df -h 显示 100% 使用 │ │ └── 无法分配新数据块 │ │ │ └── inode耗尽 (小文件很多的情况) │ ├── df -i 显示 100% 使用 │ └── 无法创建新文件 (即使磁盘还有空间) │ ├── 【保留空间机制】 │ │ │ ├── ext4默认保留5%空间给root用户 │ │ └── tune2fs -l /dev/mmcblk0p2 | grep "Reserved block count" │ │ │ └── 普通用户空间用尽时,root仍可写入 │ ├── 【诊断方法】 │ │ │ ├── df -h (数据块空间) │ │ └── /dev/mmcblk0p2 3.9G 3.8G 0M 100% /mnt/data │ │ │ ├── df -i (inode空间) │ │ └── /dev/mmcblk0p2 262144 262144 0 100% /mnt/data │ │ │ └── 查找大文件/大量小文件 │ ├── du -sh /* 2>/dev/null | sort -h │ └── find /mnt/data -type f -size +100M │ └── 【修复方法】 ├── 删除文件: rm -rf /mnt/data/large_file ├── 清理日志: truncate -s 0 /mnt/data/app.log └── 扩容分区 (需要重新分区)
2.5 根因五:文件锁/进程占用
文件被占用导致的写入失败 │ ├── 【现象】 │ │ │ ├── open() 成功,但写入失败 │ ├── 或者 open() 失败,errno = EACCES │ │ │ └── 某些情况下,文件看起来是只读的 │ ├── 【文件锁】 │ │ │ ├── 强制锁 (mandatory lock) │ │ ├── fcntl(fd, F_SETLK, &flock) │ │ └── 其他进程无法写入 │ │ │ └── 咨询锁 (advisory lock) │ └── 仅合作进程遵守 │ ├── 【进程占用】 │ │ │ ├── 其他进程打开了文件且未释放 │ │ └── 特别是以独占模式打开 (O_EXCL) │ │ │ ├── 文件正在被执行 │ │ └── 可执行文件正在运行中 │ │ │ └── 文件系统被其他挂载点占用 │ └── 有进程在目录中,导致无法卸载/重新挂载 │ ├── 【诊断方法】 │ │ │ ├── 查看文件锁 │ │ └── lsof /mnt/data/config.ini │ │ │ ├── 查看进程打开的文件 │ │ └── lsof | grep /mnt/data │ │ │ ├── 查看文件被哪些进程占用 │ │ └── fuser -v /mnt/data/ │ │ │ └── 查看文件锁详情 │ └── cat /proc/locks │ └── 【修复方法】 ├── 杀掉占用进程: kill -9 PID ├── 释放文件锁: 重启应用 └── 强制卸载: umount -l /mnt/data (lazy unmount)
三、实战诊断流程
# 完整的只读问题诊断流程 # Step 1: 确认错误类型 touch /mnt/data/test.txt # 输出: touch: cannot touch 'test.txt': Read-only file system # 或: touch: cannot touch 'test.txt': Permission denied # 或: touch: cannot touch 'test.txt': No space left on device # Step 2: 查看系统日志 dmesg | tail -30 # 重点关注: # - EXT4-fs error (文件系统损坏) # - I/O error (硬件错误) # - mmcblk0: error (eMMC错误) # Step 3: 查看挂载状态 mount | grep /mnt/data # 输出: /dev/mmcblk0p3 on /mnt/data type ext4 (ro,relatime) # 如果显示 ro,说明被只读挂载 # Step 4: 检查磁盘空间 df -h /mnt/data df -i /mnt/data # Step 5: 检查权限 ls -ld /mnt/data/ ls -l /mnt/data/ # Step 6: 检查扩展属性 lsattr /mnt/data/* # Step 7: 查看块设备状态 cat /sys/block/mmcblk0/ro # 0=可写, 1=只读 # Step 8: 查看进程占用 lsof +D /mnt/data # Step 9: 尝试重新挂载为读写 mount -o remount,rw /mnt/data # Step 10: 如果仍然失败,需要修复文件系统 umount /mnt/data fsck.ext4 -y /dev/mmcblk0p3 mount /mnt/data
四、预防措施与最佳实践
4.1 应用程序层面
// 正确的错误处理
FILE *fp = fopen("/mnt/data/config.ini", "w");
if (fp == NULL) {
switch (errno) {
case EACCES:
fprintf(stderr, "Permission denied\n");
break;
case ENOSPC:
fprintf(stderr, "No space left\n");
break;
case EROFS:
fprintf(stderr, "Read-only filesystem\n");
break;
default:
perror("fopen");
}
// 考虑降级处理: 写入/tmp或备用位置
fp = fopen("/tmp/config.ini", "w");
}
4.2 系统层面
# 1. 监控文件系统状态
# 添加cron任务,每分钟检查一次
* * * * * test -w /mnt/data || echo "WARNING: /mnt/data is read-only" | logger
# 2. 关键文件系统使用journald记录
# /etc/systemd/journald.conf
SystemMaxUse=500M
ForwardToSyslog=yes
# 3. 设置文件系统自动修复
# /etc/fstab 添加错误处理选项
/dev/mmcblk0p3 /mnt/data ext4 defaults,errors=remount-ro 0 2
# 4. 监控磁盘空间
# 设置告警阈值
df -h | awk 'NR>1 {print $5,$6}' | while read usage mount; do
if [ ${usage%\%} -gt 90 ]; then
logger -t disk "WARNING: $mount usage is $usage"
fi
done
4.3 嵌入式设备特有考虑
// 对于eMMC/SD卡,需要优雅处理写入失败
int safe_write_file(const char *path, const char *data) {
int retry = 3;
int fd;
while (retry--) {
fd = open(path, O_WRONLY | O_CREAT | O_SYNC, 0644);
if (fd < 0) {
if (errno == EROFS) {
// 文件系统只读,尝试重新挂载
system("mount -o remount,rw /mnt/data");
sleep(1);
continue;
}
return -1;
}
if (write(fd, data, strlen(data)) < 0) {
close(fd);
continue;
}
close(fd);
return 0;
}
// 所有重试失败,记录到内存
save_to_ramfs(path, data);
return -1;
}
五、总结:快速定位矩阵
| 错误现象 | 可能原因 | 快速诊断命令 | 解决方案 |
|---|---|---|---|
Read-only file system | 文件系统损坏 | dmesg \| grep error | fsck修复 |
Read-only file system | 挂载为只读 | mount \| grep ro | mount -o remount,rw |
Permission denied | 权限不足 | ls -l | chmod/chown |
Permission denied | 不可变属性 | lsattr | chattr -i |
No space left | 磁盘满 | df -h | 删除文件 |
No space left | inode满 | df -i | 删除小文件 |
Device or resource busy | 进程占用 | lsof/fuser | kill进程 |
I/O error | 硬件故障 | dmesg \| grep mmc | 更换硬件 |
核心要点:fopen和open的只读错误本质相同,都是内核返回EROFS、EACCES、ENOSPC等错误码。关键是要通过系统日志、挂载状态、磁盘空间、文件权限四个维度快速定位根因。嵌入式设备尤其要注意eMMC/SD卡的硬件稳定性和文件系统完整性。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)