关于文件系统
文件系统的组成
Linux 文件系统会为每个文件分配两个数据结构:索引节点(index node)和目录项(directory entry),它们主要用来记录文件的元信息和目录层次结构。
- 索引节点,也就是
**inode**
,用来记录文件的元信息,比如 inode 编号、文件大小、访问权限、创建时间、修改时间、数据在磁盘的位置等等。索引节点是文件的唯一标识,它们之间一一对应,也同样都会被存储在硬盘中,所以索引节点同样占用磁盘空间。 - 目录项,也就是
**dentry**
,用来记录文件的名字、索引节点指针以及与其他目录项的层级关联关系。多个目录项关联起来,就会形成目录结构,但它与索引节点不同的是,目录项是由内核维护的一个数据结构,不存放于磁盘,而是缓存在内存。
由于索引节点唯一标识一个文件,而目录项记录着文件的名,所以目录项和索引节点的关系是多对一,也就是说,一个文件可以有多个别字。比如,硬链接的实现就是多个目录项中的索引节点指向同一个文件。 :::info 目录是个文件,持久化存储在磁盘,而目录项是内核一个数据结构,缓存在内存。 :::
文件如何存储在磁盘?
磁盘读写的最小单位是扇区512B
。每次仅仅读写如此小的单位,读写效率会很低
文件系统把多个扇区组成了一个逻辑块,每次读写的最小单位就是逻辑块(数据块),Linux 中的逻辑块大小为 4KB,也就是一次性读写 8 个扇区,这将大大提高了磁盘的读写的效率。
:::warning
另外,磁盘进⾏格式化的时候,会被分成三个存储区域,分别是超级块、索引节点区和数据块区。
- 超级块,⽤来存储⽂件系统的详细信息,⽐如块个数、块⼤⼩、空闲块等等。
- 索引节点区,⽤来存储索引节点;
- 数据块区,⽤来存储⽂件或⽬录数据
加载进入内存的时机也不同
- 超级块:当文件系统挂载时进入内存;
- 索引节点区:当文件被访问时进入内存 :::
虚拟文件系统
文件系统的种类众多,而操作系统希望对用户提供一个统一的接口,于是在用户层与文件系统层引入了中间层,这个中间层就称为虚拟文件系统(Virtual File System,VFS)。
Linux支持的文件系统按照存储位置不同,可以分成三类
- 磁盘的文件系统,它是直接把数据存储在磁盘中,比如 Ext 2/3/4、XFS 等都是这类文件系统。
- 内存的文件系统,这类文件系统的数据不是存储在硬盘的,而是占用内存空间,我们经常用到的 /proc 和 /sys 文件系统都属于这一类,读写这类文件,实际上是读写内核中相关的数据。
- 网络的文件系统,用来访问其他计算机主机数据的文件系统,比如 NFS、SMB 等等。
操作系统的视角是如何把文件数据和磁盘块对应起来。
所以,用户和操作系统对文件的读写操作是有差异的,用户习惯以字节的方式读写文件,而操作系统则是以数据块来读写文件,那屏蔽掉这种差异的工作就是文件系统了。文件系统的基本操作单位是数据块。
文件的存储
文件存储在磁盘上,存放方式有两种:
链表的方式存放是离散的,不用连续的,于是就可以消除磁盘碎片,可大大提高磁盘空间的利用率,同时文件的长度可以动态扩展。根据实现的方式的不同,链表可分为「隐式链表」和「显式链接」两种形式。
:::warning
文件要以「隐式链表」的方式存放的话,实现的方式是文件头要包含「第一块」和「最后一块」的位置,并且每个数据块里面留出一个指针空间,用来存放下一个数据块的位置
隐式链表的存放方式的缺点在于无法直接访问数据块,只能通过指针顺序访问文件,以及数据块指针消耗了一定的存储空间。隐式链接分配的稳定性较差
:::
如果取出每个磁盘块的指针,把它放在内存的一个表中,就可以解决上述隐式链表的两个不足。那么,这种实现方式是「显式链接」,它指把用于链接文件各数据块的指针,显式地存放在内存的一张链接表中,该表在整个磁盘仅设置一张,每个表项中存放链接指针,指向下一个数据块号。
索引方式
索引的实现是为每个文件创建一个「索引数据块」,里面存放的是指向文件数据块的指针列表,说白了就像书的目录一样,要找哪个章节的内容,看目录查就可以。
如果文件很大,大到一个索引数据块放不下索引信息,这时又要如何处理大文件的存放呢?我们可以通过组合的方式,来处理大文件的存。
先来看看链表 + 索引的组合,这种组合称为「链式索引块」,它的实现方式是在索引数据块留出一个存放下一个索引数据块的指针,于是当一个索引数据块的索引信息用完了,就可以通过指针的方式,找到下一个索引数据块的信息。
Unix文件的实现方式
早期Unix采用组合的方式:
- 如果存放文件所需的数据块小于 10 块,则采用直接查找的方式;
- 如果存放文件所需的数据块超过 10 块,则采用一级间接索引方式;
- 如果前面两种方式都不够存放大文件,则采用二级间接索引方式;
- 如果二级间接索引也不够存放大文件,这采用三级间接索引方式;
:::warning 有点类似MySQL中索引生成的方式 :::
空闲空间管理
空闲表法
空闲表法就是为所有空闲空间建立一张表,表内容包括空闲区的第一个块号和该空闲区的块个数
这种方法仅当有少量的空闲区时才有较好的效果。因为,如果存储空间中有着大量的小的空闲区,则空闲表变得很大,这样查询效率会很低。
空闲链表法
我们也可以使用「链表」的方式来管理空闲空间,每一个空闲块里有一个指针指向下一个空闲块,这样也能很方便的找到空闲块并管理起来
其特点是简单,但不能随机访问,工作效率低,因为每当在链上增加或移动空闲块时需要做很多 I/O 操作,同时数据块的指针消耗了一定的存储空间。
类似MySQL中空闲页的管理
位图法
位图是利用二进制的一位来表示磁盘中一个盘块的使用情况,磁盘上所有的盘块都有一个二进制位与之对应。
在 Linux 文件系统就采用了位图的方式来管理空闲空间,不仅用于数据空闲块的管理,还用于 inode 空闲块的管理,因为 inode 也是存储在磁盘的,自然也要有对其管理
文件系统的结构
数据块的位图是放在磁盘块里的,假设是放在一个块里,一个块 4K,每位表示一个数据块,共可以表示 4 1024 8 = 2^15 个空闲块。
这样能表示的空间范围有限,一个数据块4K的话,最大不过128M
:::info
在 Linux 文件系统,把这个结构称为一个块组,那么有 N 多的块组,就能够表示 N 大的文件。文件系统都由大量块组组成,在硬盘上相继排布:
- 超级块,包含的是文件系统的重要信息,比如 inode 总个数、块总个数、每个块组的 inode 个数、每个块组的块个数等等。
- 块组描述符,包含文件系统中各个块组的状态,比如块组中空闲块和 inode 的数目等,每个块组都包含了文件系统中「所有块组的组描述符信息」。
- 数据位图和 inode 位图, 用于表示对应的数据块或 inode 是空闲的,还是被使用中。
- inode 列表,包含了块组中所有的 inode,inode 用于保存文件系统中与各个文件和目录相关的所有元数据。
- 数据块,包含文件的有用数据。 :::
目录的存储
基于 Linux 一切皆文件的设计思想,目录其实也是个文件,普通文件的块里面保存的是文件数据,而目录文件的块里面保存的是目录里面一项一项的文件信息。
如果目录中文件较多,保存目录的格式改成哈希表,对文件名进行哈希计算,把哈希值保存起来,如果我们要查找一个目录下面的文件名,可以通过名称取哈希。如果哈希能够匹配上,就说明这个文件的信息在相应的块里面。
软链接和硬链接
硬链接是多个目录项中的「索引节点」指向一个文件,也就是指向同一个 inode,但是 inode 是不可能跨越文件系统的,每个文件系统都有各自的 inode 数据结构和列表,所以硬链接是不可用于跨文件系统的。
软链接相当于重新创建一个文件,这个文件有独立的 inode,但是这个文件的内容是另外一个文件的路径,所以访问软链接的时候,实际上相当于访问到了另外一个文件,所以软链接是可以跨文件系统的
01. stat、fstat、fstatat和lstat
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
#include <fcntl.h> /* Definition of AT_* constants */
#include <sys/stat.h>
int fstatat(int dirfd, const char *pathname, struct stat *statbuf, int flags);
pathname和fd指明当前的路径,可以是绝对路径也可以是相对路径,有多种组合方式
statbuf是一个指针,作为返回值。
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* Total size, in bytes */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B blocks allocated */
/* Since Linux 2.6, the kernel supports nanosecond
precision for the following timestamp fields.
For the details before Linux 2.6, see NOTES. */
struct timespec st_atim; /* Time of last access */
struct timespec st_mtim; /* Time of last modification */
struct timespec st_ctim; /* Time of last status change */
#define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};
02. 文件类型
- 普通文件
- 目录文件
- 块特殊文件,这种类型文件提供对设备(如磁盘)的带缓冲访问
- 字符特殊文件,提供对设备的不带缓冲访问
- FIFO,用于进程间通信或者说是命名管道
- 套接字
- 符号链接
文件类型信息包含在stat结构的st_mode成员中,可以通过宏来确定文件类型
e.g.,if(S_ISREG(buf.st_mode))
03. 函数umask
umask函数为进程设置文件模式创建屏蔽字,并且返回之前的值
#include<sys/stat.h>
mode_t umask(mode_t cmask);
进程创建一个新文件或者新目录时,就一定会使用文件模式创建屏蔽字(open和create函数的参数mode,指定了新文件的访问权限位)。umask可以将指定的位给屏蔽。
umask(0);
umask(S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH);//禁止其他组和用户访问权限
04. chmod
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
#include <fcntl.h> /* Definition of AT_* constants */
#include <sys/stat.h>
int fchmodat(int dirfd, const char *pathname, mode_t mode, int flags);
chmod函数在指定文件上操作,fchmod则是对已经打开的文件操作,fchmodat根据相对路径或者绝对路径操作。
功能都是改变现有文件的访问权限
05. 函数link、linkat、ulink、ulinkat和remove
这几个函数是实现硬链接的,或者说将新的目录项指向给定的文件的inode的
NAME
link, linkat - make a new name for a file
SYNOPSIS
#include <unistd.h>
int link(const char *oldpath, const char *newpath);
#include <fcntl.h> /* Definition of AT_* constants */
#include <unistd.h>
int linkat(int olddirfd, const char *oldpath,
int newdirfd, const char *newpath, int flags);
返回值:若成功返回0,否则返回-1
虽然POSIX允许跨越文件系统链接,但是大多数实现要求新建的和现有的路径名在同一个文件系统中。
删除一个现有的目录项用unlink函数
NAME
unlink, unlinkat - delete a name and possibly the file it refers to
SYNOPSIS
#include <unistd.h>
int unlink(const char *pathname);
#include <fcntl.h> /* Definition of AT_* constants */
#include <unistd.h>
int unlinkat(int dirfd, const char *pathname, int flags);
返回值:若成功返回0,否则返回-1
这两个函数所作的仅仅是删除目录项,并且将文件的链接计数-1,只有当链接计数为0时,文件才可以被删除
06. 创建和读取符号链接
符号链接、或者说软连接,和硬链接的区别在于他并不是直接让目录项指向inode;
详情见
创建符号链接
NAME
symlink, symlinkat - make a new name for a file
SYNOPSIS
#include <unistd.h>
int symlink(const char *target, const char *linkpath);
#include <fcntl.h> /* Definition of AT_* constants */
#include <unistd.h>
int symlinkat(const char *target, int newdirfd, const char *linkpath);
事实上target并不一定已经存在,并且linkpath和target可以不在同一个文件系统中。
因为open函数跟随符号链接,所以需要有一种方法打开链接本身
NAME
readlink, readlinkat - read value of a symbolic link
SYNOPSIS
#include <unistd.h>
ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
#include <fcntl.h> /* Definition of AT_* constants */
#include <unistd.h>
ssize_t readlinkat(int dirfd, const char *pathname,
char *buf, size_t bufsiz);