← 博客

File System

文件系统

inode

每个文件都有唯一的 inode, 只在当前文件系统唯一

存放的信息有

  • 文件权限(File Permissions):读(r=read)、写(w=write)、执行(x=execute),示例:rw-r–r–(所有者可读可写,所属组和其他用户仅可读)
  • 文件所有者和所属组(File Owner & Group):
    • 所有者 UID(User ID)
    • 所属组 GID(Group ID)
  • 文件大小(File Size):单位字节(Byte),区分普通文件(存储内容大小)和目录文件(存储目录项的大小)
  • 时间戳(Timestamps):3 个核心时间属性
    • ctime(Change Time):inode 元数据修改时间(如权限、所有者变更)
    • mtime(Modify Time):文件内容修改时间
    • atime(Access Time):文件内容最后访问时间
  • 数据块指针(Data Block Pointers):最核心字段,直接或间接指向存储文件内容的数据块物理地址
  • 链接数(Link Count):指向该 inode 的硬链接数量,默认新建文件时 link count = 1

读取普通文件的流程

  • 解析路径:从根目录 / 开始,依次查找目录项的 inode
  • 找到文件后,从数据块指针找到文件内容的地址

读取软链接文件的流程

  • 同样步骤,读取文件内容,发现这是一个链接文件,则读它的数据块内容(原文件链接)
  • 重新解析路径,重复过程

硬链接 vs 软链接

硬链接

  • 给文件起一个别名
  • 删除原文件之后,链接数-1,但只有链接数>0,inode 还在,原文件还能访问
  • 不能指向目录 软链接
  • 软链接:创建一个新文件(有自己的 inode),这个文件的内容是「原文件的路径」

file descriptor

文件描述符是在内存中的。每个进程的内核空间中,有一个file descriptor table, PCB 中有指向这个表的指针。

OS 维护了一张Open File Table,里面记录一系列指针,每个指针指向这些文件的 inode。这张表对所有进程可见

当某进程打开一个文件时,OS 分配一个 file descriptor, 同时在 open files table中加上指向该文件 inode 的指针。
接着进程的file descriptor table里面增加一个指向 open files table中某指针的指针

default file descriptor

stdin(0), stdout(1), stderr(2)

每个进程的默认 file descriptor 上限为 1024

page cache

操作系统在物理内存中,开辟了一片缓存空间。每个 page 的大小是 4KB,存放最近访问的文件数据块

  • 读加速:读文件时,优先读 page cache
  • 写缓冲:写文件时,新数据先写入 page cache,被写了的也标记为脏页。操作系统通过后台异步线程,定期把脏页数据刷盘,刷盘完之后,再把脏页标记取消

fsync 属于主动刷盘策略

Direct IO

Direct IO是Linux 内核提供的一种绕开 Page Cache 的文件 IO 方式,数据直接在用户程序缓冲区和磁盘设备之间传输,不经过内核的 Page Cache 中转适合大文件顺序读写。

IO 模型

read 操作分为两个阶段

  • 把数据从网卡拷贝到内核缓冲区
  • 把数据从内核缓冲区拷贝到用户缓冲区

Blocking IO

read()之后,在第一阶段,进程被 CPU 挂起。等到内核将数据拷贝到内核缓冲区之后,唤醒进程,内核再操作第二阶段的拷贝。等到两个阶段完成,进程才可执行自己的代码。

Non-blocking IO

轮询。执行 read()后,若第一阶段的数据没有准备好,直接返回失败,过段时间再询问

第二阶段阻塞

IO Multiplexing

进程首先调用 select/poll/epoll, 同时监听多个 IO 描述符的状态,然后被阻塞
当任意一个被监听的描述符数据准备完成,内核唤醒进程
进程执行 read(),直接进行第二阶段的内存拷贝

Signal-driven IO

  • 进程通过 sigaction() 注册一个信号处理函数(通常是 SIGIO 信号); 发起 IO 请求后,进程不阻塞,可以继续执行其他任务;
  • 当内核完成阶段 1(数据准备),会向进程发送 SIGIO 信号;
  • 进程收到信号后,在信号处理函数中发起 read() 调用,执行阶段 2 的阻塞拷贝。

Asynchronous IO

  • 进程执行系统调用aio_read/aio_write,传入用户缓冲区地址和信号/回调函数
  • 内核完成接下来的两个阶段
  • 当内核完成所有阶段,会通过信号或回调函数通知进程 “IO 已完成”

IO 多路复用

select

  • process 给内核三个 fd 集合(bitset),分别是等待读 fd, 等待写 fd, 等待异常 fd
  • 内核轮询这些 fd,一旦有事件发生,就给给这些 fd 置位,然后把这些 fd 拷贝回用户空间
  • process 遍历这些 fd,检查哪个有事件,并且处理第二阶段的数据拷贝
  • fd 数量上限 1024, 需要拷贝整个bitset

poll

  • 无 fd 数量上限
  • 需要拷贝整个 数组

epoll

  • Linux 专有,不跨平台
  1. process 调用epoll_create,在内核中创建 epoll 对象,返回一个文件描述符 epfd. 此时内核中的 epoll对象由两部分组成:红黑树(所有注册的 fd),链表(所有就绪的 fd)
  2. epoll_ctl(),注册/修改/删除监听的 fd
  3. epoll_wait()阻塞进程.返回此时活跃的 fd 数量,并且修改 events数组,process 直接遍历前N个元素即可,不用遍历所有 fd
    1
    2
    3
    4
    5
    
    // epfd:epoll 对象的文件描述符
    // events:输出参数,内核会把就绪的事件写入这个数组
    // maxevents:events 数组的最大长度(最多接收多少个就绪事件)
    // timeout:超时时间(-1=无限等待,0=立即返回,>0=等待毫秒数)
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);