linux内核

Posted by fjw on September 5, 2022

文件系统

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 设计思想

  1. 一切皆文件(对象)思想 普通文件、目录、设备(/dev/sda)、socket、pipe、eventfd、inotify、bpf地图、cgroup、tracefs、debugfs…… 全都用 inode + dentry 表示,都能走同样的 open/read/write 接口。

  2. Dentry cache + inode cache 路径查找极快(99%+ 命中 Dcache)。 inode 也有 slab 缓存,频繁访问的文件元数据基本不落盘。

  3. 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");
    
  4. Mount namespace + per-process root 每个进程(或进程组)可以有自己的根文件系统视图(pivot_root、容器最核心依赖)。

  5. 叠加文件系统天然支持 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(循环设备,用于挂载镜像)。

设备文件是文件类型中的子集,但它们桥接了硬件和用户空间。内核通过设备驱动(模块,如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 等)