Linux文件系统

这篇文章讲文件系统比较通俗形象,由浅入深讲述了inode与block的概念,直接索引与多级索引寻址能力,为什么ext2文件系统最大单文件大小是4T,稀疏文件的概念,还不错:深度剖析 Linux cp 命令的秘密

基础

Linux 文件系统会为每个文件分配两个数据结构:索引节点(index node)和目录项(directory entry),它们主要用来记录文件的元信息和目录层次结构。

  • 索引节点(inode):用来记录文件的元信息,比如 inode 编号、文件大小、访问权限、创建时间、修改时间、数据在磁盘的位置等等。索引节点是文件的唯一标识,它们之间一一对应,也同样都会被存储在硬盘中,所以索引节点同样占用磁盘空间。
  • 目录项(dentry):用来记录文件的名字、索引节点指针以及与其他目录项的层级关联关系。多个目录项关联起来,就会形成目录结构,但它与索引节点不同的是,目录项是由内核维护的一个数据结构,不存放于磁盘,而是缓存在内存

目录与目录项:

  • 目录也是文件,也是用索引节点唯一标识,和普通文件不同的是,普通文件在磁盘里面保存的是文件数据,而目录文件在磁盘里面保存子目录或文件。
  • 目录持久化存储在磁盘,而目录项是内核一个数据结构,缓存在内存

磁盘被分成三个存储区域,分别是超级块、索引节点区和数据块区。

  • 超级块,用来存储文件系统的详细信息,比如块个数、块大小、空闲块等等(当文件系统挂载时进入内存)
  • 索引节点区,用来存储索引节点(当文件被访问时进入内存)
  • 数据块区,用来存储文件或目录数据;

/etc/passwd的权限为 -rw-r--r-- 也就是说:

  • 该文件的所有者拥有读写的权限
  • 用户组成员只有查看权限
  • 其它成员只有查看的权限。

VFS

VFS(Virtual File System)虚拟文件系统扮演着文件系统管理者的角色,与它相关的数据结构只存在于物理内存当中。它的作用是:屏蔽下层具体文件系统操作的差异,为上层的操作提供一个统一的接口。

Linux 支持的文件系统也不少,根据存储位置的不同,可以把文件系统分为三类:

  • 磁盘的文件系统,它是直接把数据存储在磁盘中,比如 Ext 2/3/4、XFS 等都是这类文件系统。
  • 内存的文件系统,这类文件系统的数据不是存储在硬盘的,而是占用内存空间,我们经常用到的 /proc 和 /sys 文件系统都属于这一类,读写这类文件,实际上是读写内核中相关的数据数据。
  • 网络的文件系统,用来访问其他计算机主机数据的文件系统,比如 NFS、SMB 等等。

文件的使用

系统调用:open系统调用打开文件返回fd,write向fd写数据,最后close这个fd

用户空间write()–》虚拟文件系统sys_write()–》文件系统的写方法–》磁盘

fd是文件描述符,操作系统会跟踪进程打开的所有文件

文件的存储

连续空间存放方式:

  • 优点:读写效率高,文件头需要指定起始块位置与长度,linux的inode就支持
  • 缺点:磁盘碎片、文件长度不易扩展

非连续空间存放方式:

  • 链表:可消除磁盘碎片,文件长度可扩展。
    • 隐式链接:文件头包含第一个数据块和最后一个数据块,缺点是得遍历查找以及指针也占空间
    • 显式链接:取出每个数据块的指针,存放在内存的文件分配表里,缺点是不适合大磁盘
  • 索引:可消除磁盘碎片,文件长度可扩展,也适合大磁盘
    • 为每个文件创建一个「索引数据块」,里面存放的是指向文件数据块的指针列表,就想书的目录一样
    • 文件头需要包含指向『索引数据块』的指针
    • 创建文件时,索引块的所有指针都设为空。当首次写入第 i 块时,先从空闲空间中取得一个块,再将其地址写到索引块的第 i 个条目。
    • 优点:文件的创建、增大、缩小很方便;不会有碎片问题;支持随机读写
  • 链表+索引:『链式索引块』
    • 在索引数据块留出一个存放下一个索引数据块的指针,,于是当一个索引数据块的索引信息用完了,就可以通过指针的方式,找到下一个索引数据块的信息(感觉有点像跳表)
  • 索引+索引:『多级索引块』
    • 通过一个索引块来存放多个索引数据块,一层套一层索引,像极了俄罗斯套娃

空闲空间管理

空闲表法就是为所有空闲空间建立一张表,表内容包括空闲区的第一个块号和该空闲区的块个数,注意,这个方式是连续分配的,缺点也是显而易见的,如果有大量的小的空闲区,则空闲表就会变得很大

空闲链表可以应付连续的小空闲区,但是不能随机访问

位图是利用二进制的一位来表示磁盘中一个盘块的使用情况,磁盘上所有的盘块都有一个二进制位与之对应。当值为 0 时,表示对应的盘块空闲,值为 1 时,表示对应的盘块已分配。

Linux 文件系统就采用了位图的方式来管理空闲空间,不仅用于数据空闲块的管理,还用于 inode 空闲块的管理

Page Cache 层

  • 引入 Cache 层的目的是为了提高 Linux 操作系统对磁盘访问的性能。Cache 层在内存中缓存了磁盘上的部分数据。
  • 在 Linux 的实现中,文件 Cache 分为两个层面,Page Cache主要是对文件的缓存;Buffer Cache则主要是对块设备的缓存,
  • cache两大功能:预读(局部性原理)和回写(数据暂存buffer,然后统一异步落盘)

软链接与硬链接

为解决文件的共享使用,Linux系统引入了两种链接:硬链接与软链接(又称符号链接)。链接为Linux系统解决了文件的共享使用,还带来了隐藏文件路径、增加权限安全及节省存储等好处。

  • 若一个inode号对应多个目录项的『索引节点』,则称这些文件为硬链接。

    • 文件有相同的inode及data block;
    • 只能对已存在的文件进行创建
    • 不能对目录进行创建,只可对文件创建;
    • 删除一个硬链接文件并不影响其他有相同 inode 号的文件。
  • 若文件用户数据块中存放的内容是另一文件的路径名的指向(类似于windows的快捷方式),则该文件就是软连接。软链接就是一个普通文件,只是数据块内容有点特殊。

    • 软链接有着自己的inode号以及data block
    • 可对不存在的文件或目录创建软链接;
    • 软链接可对文件或目录创建;
    • 删除软链接并不影响被指向的文件,但若被指向的原文件被删除,则相关软连接被称为死链接(即 dangling link,若被指向路径文件被重新创建,死链接可恢复为正常的软链接)。

发现:硬链接和软链接有点像引用和指针的差别,

  • 引用必须初始化(硬链接只能对已存在的文件),指针可以不初始化(软链接课对不存在的文件)
  • 引用就是别名(硬链接也是别名),指针是对象的地址(软链接是另一文件的路径名的指向)
  • 引用不是对象(硬链接的inode与data block相同),指针是对象(软链接的inode独立)

ln -s / /home/good/linkname:链接根目录到 /home/good/linkname

加参数-s就是软链接,不加参数-s就是硬链接

文件IO

标准库缓冲

  • 缓冲IO:利用的是标准库的缓存实现文件的加速访问,而标准库再通过系统调用访问文件。
  • 非缓冲 I/O,直接通过系统调用访问文件,不经过标准库缓存。
  • 缓冲特指标准库内部实现的缓冲,程序遇到换行才输出,这就是被标准库暂时缓存了起来,这样可以减少系统调用的次数

操作系统缓冲

  • 直接 I/O,不会发生内核缓存和用户程序之间数据复制,而是直接经过文件系统访问磁盘。
  • 非直接 I/O,读操作时,数据从内核缓存中拷贝给用户程序,写操作时,数据从用户程序拷贝给内核缓存,再由内核决定什么时候写入数据到磁盘。
  • 我们都知道磁盘 I/O 是非常慢的,所以 Linux 内核为了减少磁盘 I/O 次数,在系统调用后,会把用户数据拷贝到内核中缓存起来,这个内核缓存空间也就是「页缓存」,只有当缓存满足某些条件的时候,才发起磁盘 I/O 的请求。
  • 如果你在使用文件操作类的系统调用函数时,指定了 O_DIRECT 标志,则表示使用直接 I/O。如果没有设置过,默认使用的是非直接 I/O。
  • 以下几种场景会触发内核缓存的数据写入磁盘:
    • 在调用 write 的最后,当发现内核缓存的数据太多的时候,内核会把数据写到磁盘上;
    • 用户主动调用 sync,内核缓存会刷到磁盘上;
    • 当内存十分紧张,无法再分配页面时,也会把内核缓存的数据刷到磁盘上;
    • 内核缓存的数据的缓存时间超过某个时间时,也会把数据刷到磁盘上;

阻塞与非阻塞

  • 阻塞 I/O,例如当用户程序执行 read ,线程会被阻塞,一直等到内核数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区中,当拷贝过程完成,read 才会返回。(注意,阻塞等待的是「内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程
  • 非阻塞 I/O,非阻塞的 read 请求在数据未准备好的情况下立即返回,可以继续往下执行,此时应用程序不断轮询内核,直到数据准备好,内核将数据拷贝到应用程序缓冲区,read 调用才可以获取到结果
  • 每次轮询有点低效,IO多路复用的技术就出来了,当内核数据准备好时,再以事件通知应用程序进行操作,可以极大地改善CPU的利用率,应用进程可以使用CPU做其他事

同步与异步

  • 之前说的都是同步,因为在最后一次read系统调用时,应用进程必须等待内核将数据从内核空间拷贝到进程自己的虚拟地址空间,这个过程是同步
  • 真正的异步:『内核数据准备好」和「数据从内核态拷贝到用户态」这两个过程都不用等待,当发起aio_read,立即返回,内核自动将数据从内核空间拷贝到应用程序空间,这个拷贝过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作

预读

  • readahead,检查当前IO与上个IO的文件偏移量,判断当前是否是顺序读,如果是的话,就执行额外的读请求,填充进缓存中