文章目录
- 1、概述
- 2、inode结构
- 3、block BitMap 和 inode BitMap
- 4、软链接和硬链接
- 4.1 硬链接
- 4.2 软链接
- 5、Linux下的文件类型的
1、概述
文件存储在硬盘上,硬盘的最小存储单位叫做“扇区”(Sector)。每个扇区储存512字节
操作系统读取硬盘的时候,不会一个个扇区的读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个“块”(block)。这种由多个扇区组成的“块”,是文件存取的最小单位。“块”的大小,最常见的是4KB,即连续八个sector组成一个block。
操作系统与内存进行交互时,是按照页为基本单位的,一页也就等于4KB,这里和block的大小一致,我猜应该是为了方便对齐吧。
文件=文件属性+文件内容
文件内容都存在于block中,而文件属性存放在文件控制块中,在Linux下又称为inode(index node),它主要就存放文件的创建者、文件的创建日期、文件的大小等信息。
以ext系列文件系统为例,看看它们主要包含哪些结构:
Boot Block:文件系统的起始位置,存储了一些重要的引导信息,包括文件系统的元数据和加载程序等。
每一个Block Group都是一个分区,每个分区都可以实现不同的文件系统,这里是以ext系列的文件系统为例。
Superblock(超级块):在ext2/ext3/ext4文件系统中,Superblock存储了文件系统的整体信息,如文件系统的类型、大小、空闲和已填满的块数量等。Superblock通常位于文件系统的第一个块组中。
Group Descriptor Table(组描述符表):描述了文件系统中每个块组的属性,如块组的块大小、块数量等。每个块组都有一个与之关联的组描述符,存储在组描述符表中。
Block BitMap:用于记录Block块的使用情况。
inode BitMap:用于记录inode的使用情况。
Inode Table(索引节点表):在ext2/ext3/ext4文件系统中,Inode Table存储了文件系统的索引信息,包括文件的元数据(如文件大小、创建时间等)和文件数据块的地址。
Data Blocks(数据块):存储了文件系统的实际数据,这些数据块是文件系统中的基本存储单元。
以上的数据都是会占用磁盘空间的
2、inode结构
前面谈到文件=文件属性+文件内容
一般来说,文件属性和内容都是分开存储的,存放属性的结构又叫文件控制块,在Linux下,文件控制块就是inode
以下是Linux-4.9.145中定义的,文件系统ext4的inode结构体
//linux-4.9.145\fs\ext4\ext4.h
/*
* Structure of an inode on the disk
*/
struct ext4_inode {
__le16 i_mode; /* File mode */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i_size_lo; /* Size in bytes */
__le32 i_atime; /* Access time */
__le32 i_ctime; /* Inode Change time */
__le32 i_mtime; /* Modification time */
__le32 i_dtime; /* Deletion Time */
__le16 i_gid; /* Low 16 bits of Group Id */
__le16 i_links_count; /* Links count */
__le32 i_blocks_lo; /* Blocks count */
__le32 i_flags; /* File flags */
union {
struct {
__le32 l_i_version;
} linux1;
struct {
__u32 h_i_translator;
} hurd1;
struct {
__u32 m_i_reserved1;
} masix1;
} osd1; /* OS dependent 1 */
__le32 i_block[EXT4_N_BLOCKS];/* Pointers to blocks */
__le32 i_generation; /* File version (for NFS) */
__le32 i_file_acl_lo; /* File ACL */
__le32 i_size_high;
__le32 i_obso_faddr; /* Obsoleted fragment address */
union {
struct {
__le16 l_i_blocks_high; /* were l_i_reserved1 */
__le16 l_i_file_acl_high;
__le16 l_i_uid_high; /* these 2 fields */
__le16 l_i_gid_high; /* were reserved2[0] */
__le16 l_i_checksum_lo;/* crc32c(uuid+inum+inode) LE */
__le16 l_i_reserved;
} linux2;
struct {
__le16 h_i_reserved1; /* Obsoleted fragment number/size which are removed in ext4 */
__u16 h_i_mode_high;
__u16 h_i_uid_high;
__u16 h_i_gid_high;
__u32 h_i_author;
} hurd2;
struct {
__le16 h_i_reserved1; /* Obsoleted fragment number/size which are removed in ext4 */
__le16 m_i_file_acl_high;
__u32 m_i_reserved2[2];
} masix2;
} osd2; /* OS dependent 2 */
__le16 i_extra_isize;
__le16 i_checksum_hi; /* crc32c(uuid+inum+inode) BE */
__le32 i_ctime_extra; /* extra Change time (nsec << 2 | epoch) */
__le32 i_mtime_extra; /* extra Modification time(nsec << 2 | epoch) */
__le32 i_atime_extra; /* extra Access time (nsec << 2 | epoch) */
__le32 i_crtime; /* File Creation time */
__le32 i_crtime_extra; /* extra FileCreationtime (nsec << 2 | epoch) */
__le32 i_version_hi; /* high 32 bits for 64-bit version */
__le32 i_projid; /* Project ID */
};
一些相关字段的说明:
i_mode:该字段表示文件的访问权限和类型。它包括读取、写入和执行权限的信息,以及条目是否为常规文件、目录、符号链接等
i_uid和i_gid:分别存储文件所有者的用户ID(UID)和组ID(GID)
i_size_lo:文件的大小
i_atime:记录最后一次访问文件的时间
i_ctime:记录最后一次修改文件属性(大小,创建时间,创建者等信息)的时间/或者叫做最后一次修改inode的时间 (i_mtime的改变必然会引起i_ctime的改变,因为文件的大小发生了改变)
i_mtime:记录最后一次修改文件内容的时间
i_dtime:文件删除的事件
i_links_count:硬链接的数量
i_blocks_lo:使用了多少的Block块
__le32 i_block[EXT4_N_BLOCKS]:一个指针数组,里面的元素是指向Block块的物理地址
在Linux下,使用命令 stat + 文件名,就可以查看文件的详细信息
Size: 文件的大小,以字节为单位。
Blocks: 被用于存储文件的数据块的数量。
IO Block: 这是用于文件读写的内存块的大小。
Device: 文件所在的设备。
Inode: 文件的inode编号。
Links: 硬链接的数量。
Access: 最后一次访问文件的时间。
Modify: 最后一次修改文件的时间。
Change: 最后一次更改文件的inode信息的时间。
Birth: 文件的创建时间,因为test.c没有被记录(所以显示为"-")。
在Linux中,每创建一个文件,都会与一个inode对应,并且Linux系统内部不使用文件名,而是使用inode编号识别文件,文件名和inode编号的映射关系是存放在数据块中(Block),对于系统来说文件名只是inode编号便于识别的别称。除此之外,inode编号就对应着inode table的下标。
查看一个文件的inode编号
通过下面这张图,就能进一步的理解它们之间的关系:
因为文件的大小不会仅仅就使用一个block,也就是4kb,有可能会使用很多个block,因此ext4_inode中有这样一个字段,__le32 i_block[EXT4_N_BLOCKS],它是一个指针数组,用来存放block的地址的,但这个数组的大小仅仅为15
/*
* Constants relative to the data blocks
*/
#define EXT4_NDIR_BLOCKS 12
#define EXT4_IND_BLOCK EXT4_NDIR_BLOCKS
#define EXT4_DIND_BLOCK (EXT4_IND_BLOCK + 1)
#define EXT4_TIND_BLOCK (EXT4_DIND_BLOCK + 1)
#define EXT4_N_BLOCKS (EXT4_TIND_BLOCK + 1)
所以对多只能存储15*block(4个kb)=60kb的数据,如果存储的数据大于60kb呢?如何存储呢?
那么在最后一个block中,会存放下一个block的地址,如果下一个block也不够用,那么它又会存放下下一个block的地址,以此类推,就形成了链式结构。
并且ext4_inode中,i_blocks_lo和i_size_lo分别表示使用的block块数和总的文件大小(包括inode(大小固定,一般为128字节)和blocks),因此在读取文件时,就能知道是否读到了最后一个block,并且最后一个block应该读取多少字节
假设一个文件需要用到17个block,那么它的存储形式如下:
此外还有一个问题?
想要查找一个文件,就需要找到它的inode编号,而文件名称和inode编号的映射关系是存放在当前目录下的block中的,这样就需要查找当前目录,而当前目录的名称和inode编号的映射关系又存放在上级目录的block中,这样就会形成递归查找,总会查找根目录。然而根目录已经没有上级目录,那么就无法查找到根目录的inode节点?
例如现在有一个文件fl.txt,文件内容为hello fl,目录结构为 /root/fl.txt,如果想要查看这个文件,就必须递归式的查找,直到查找到根目录为止
但是根目录没有上级目录,如何知道根目录的inode编号呢?
为了解决这个问题,大家就将根目录的inode编号设定为一个固定值,在Linux系统中,根目录的inode编号通常是2,Linux下文件系统的都必须遵守这个规定
3、block BitMap 和 inode BitMap
在创建文件时,无论是申请inode还是block,是如何知道对应的inode或者block是否被占用呢?
这里就采用了bitmap这样的数据结构,通过二进制的0和1判断是否被占用,具体的原理可以看一下我的另外一篇博客位图的实现原理
block BitMap用于判断block是否被占用
inode BitMap 用于判断inode是否被占用
BitMap只能快速判断某个block或者inode是否被使用,如果想要申请空闲的block或者inode,就需要遍历整个bitmap,时间复杂度就是O(n),对此内核一定做了其他的优化,例如使用hash结构,这个我暂时不知道,等后面知道了,再回来补充。
因此对应创建文件test.c,本质就是修改位图+填充inode信息
- 遍历inode bitmap位图结构,找到空闲数据块;
- 把test.c的inode填入inode数据块里,由于是空文件所以不需要block block。
而需要写入内容时:根据其inode找到它对应的空间,发现为空,就通过遍历block bitmap位图结构找到空闲的block,然后把该block的地址写入到inode结构中的i_block数组中,然后就把文件内容写到block即可。
对于删除文件而言
并不需要清空inode和block的内容,只需要修改两个位图,由1->0即可“删除该文件”,这也就将该位置定义为可覆盖的文件数据;这也就是为什么平时删除文件是可以恢复的。
4、软链接和硬链接
为解决文件的共享使用,Linux 系统引入了两种链接:硬链接 (hard link) 与软链接(又称符号链接,即 soft link 或 symbolic link)。链接为 Linux 系统解决了文件的共享使用,还带来了隐藏文件路径、增加权限安全及节省存储等好处。
4.1 硬链接
linux下的文件是通过索引节点(Inode)来识别文件的,硬链接可以认为是一个指针,指向文件索引节点的指针,系统并不为它重新分配inode。每添加一个一个硬链接,ext4_inode结构中的i_links_count就会增加1
硬连接之间没有主次之分,删除某个硬链接,只是将其从目录的数据块中删除相关信息,并且文件链接数减一。不会从inode表中删除inode,除非只剩下一个链接数。
举例说明:
#创建一个目录
[root@VM-4-4-centos ~]# mkdir hardlink
#查看该目录的文件详细信息,可以看到对应的硬链接数为2
[root@VM-4-4-centos ~]# stat hardlink/
File: ‘hardlink/’
Size: 4096 Blocks: 8 IO Block: 4096 directory
Device: fd01h/64769d Inode: 451124 Links: 2
Access: (0755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2023-12-12 08:53:26.917803512 +0800
Modify: 2023-12-12 08:53:26.917803512 +0800
Change: 2023-12-12 08:53:26.917803512 +0800
Birth: -
#进入该目录,并创建文件
[root@VM-4-4-centos ~]# cd hardlink/
[root@VM-4-4-centos hardlink]# touch fl.txt
#查看该文件的硬链接数,为1
[root@VM-4-4-centos hardlink]# ll fl.txt
-rw-r--r-- 1 root root 0 Dec 12 08:54 fl.txt
#向fl.txt写内容
[root@VM-4-4-centos hardlink]# echo "hell fl" > fl.txt
[root@VM-4-4-centos hardlink]# cat fl.txt
hell fl
#为fl.txt创建一个硬链接,再次查看文件的硬链接数,结果都为2,并且它们的inode编号都是一样的
[root@VM-4-4-centos hardlink]# ln fl.txt fl.link
[root@VM-4-4-centos hardlink]# ll -i
total 8
422632 -rw-r--r-- 2 root root 8 Dec 12 08:56 fl.link
422632 -rw-r--r-- 2 root root 8 Dec 12 08:56 fl.txt
#删除fl.txt,fl.link依旧可以正常查看内容
[root@VM-4-4-centos hardlink]# rm -f fl.txt
[root@VM-4-4-centos hardlink]# cat fl.link
hell fl
#再次查看fl.link的硬链接数,结果为1
[root@VM-4-4-centos hardlink]# ll
total 4
-rw-r--r-- 1 root root 8 Dec 12 08:56 fl.link
当我们删除fl.txt时,只是把hardlink所对应的block里面的fl.txt和所对应的inode编号的映射关系给删除了,但此时还有fl.link和所对应的inode编号的映射关系,因此我们依旧能找到fl.link所对应的inode,以及inode对应的block,从而就能找到里面的数据。如图所示:
通过这张图,同时也验证了在Linux下是使用inode编号识别文件,而非文件名
当删除了fl.txt之后,fl.link的硬链接数为1,如果再将fl.link也删除,对应的inode也会被回收。具体的操作就是,在block BitMap中,将inode所使用的block所对应的位置改为0,表示改block块空闲,再将inode结构中的i_blocks_lo改为0,表示inode不再关联任何block,最终在inode BitMap,将inode所对应的位置改为0,表示该inode空闲。
这也说明了,Linux下删除文件,只是将inode和block所处BitMap的位置,从1改为0,表示空闲,并解除inode和block的关联,而真正的数据是不会被删除的,直到新数据的覆盖,因此Linux中被删除的文件数据是可以被找回的,只要相应的数据块没有被再次覆盖使用
这里还有一个问题,为什么目录hardlink的硬链接数为2呢?
因为在hardlink目录下,有一个名为.
的隐藏目录,而该目录也就是当前目录的意思。即hardlink目录。所以才会有两个链接数
[root@VM-4-4-centos hardlink]# ll -a
total 12
drwxr-xr-x 2 root root 4096 Dec 12 09:00 .
dr-xr-x---. 39 root root 4096 Dec 12 08:53 ..
-rw-r--r-- 1 root root 8 Dec 12 08:56 fl.link
硬链接存在的问题
无法跨分区,跨设备创建硬链接
[root@VM-4-4-centos hardlink]# df -h .
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 40G 33G 4.8G 88% /
#当前目录所在的分区为/dev/vda1,在分区/sys/下,为当前目录下的fl.link建立一个硬链接,结果为失败
[root@VM-4-4-centos hardlink]# ln fl.link /sys/link
ln: failed to create hard link ‘/sys/link’ => ‘fl.link’: Invalid cross-device link
因为每个分区都有自己的文件系统,inode的查找规则也不一样。假设A分区的文件在B分区做了一个硬链接,此时访问B分区的此链接,按照我们想的是需要它访问A分区的inode,进行数据查询,但是它只会根据B分区的inode,在B数据块中查找数据。就相当于数据库中的两张表,你不可能拿A表的主键去在B表中查找数据。
无法创建目录的硬链接
[root@VM-4-4-centos ~]# ln hardlink/ hardlink.link
ln: ‘hardlink/’: hard link not allowed for directory
无法创建目录的硬链接,因为这可能把目录树变为环形图。例如假设存在目录 /A/B.link 和 /B/A.link。如果B.link是B目录的硬链接,A.link是A目录的硬链接。那A.link既然是/A的链接,那它里面肯定有B.link。同理B.link里面肯定有A.link。这样依次循环 /A/B.link/A.link/B.link/A.link/…。就造成了死循环的现象。
4.2 软链接
为了克服硬链接的这些限制,从而引入了软链接,也称符号链接。它相当于我们 Windows 中的快捷方式,即如果你软链接一个目录,只是一个目录的快捷方式到指定位置,操作系统找这个快捷方式会直接找到真实目录下的文件。
举例说明:
[root@VM-4-4-centos hardlink]# touch test.txt
[root@VM-4-4-centos hardlink]# echo "hello fl" > test.txt
#为test.txt创建一个软链接
[root@VM-4-4-centos hardlink]# ln -s test.txt test.slink
[root@VM-4-4-centos hardlink]# ll -i
total 4
422669 lrwxrwxrwx 1 root root 8 Dec 12 10:17 test.slink -> test.txt
422658 -rw-r--r-- 1 root root 9 Dec 12 10:16 test.txt
如上可以看出,软连接与原文件并不是同一inode,链接数也没有增加。并且文件大小也不一样。
因为软链接的inode指向的block保存的是原文件的路径,如果保存的不是路径,而是文件名,默认会在软链接所在路径查找
当原文件被删除,软链接里面存放的路径也就是失效了,软链接从而也就失效了
创建软链接时,既能使用绝对路径创建,也能使用相对路径创建
当使用相对路径创建软链接时,需要注意如下问题:
在上图中,通过 ln -s …/test.txt /tmp/test.slink,在/tmp下创建了软链接,当试图通过test.slink查看test.txt文件的内容时,发现竟然报错,原因是找不到test.txt。因为/tmp/test.slink在指向…/test.txt 的过程中,它会以自己的路径为初始点去寻找test.txt。即 /tmp/test.slink -> …/test.txt,在系统看来,它会理解成以test.slink所在路径为起点,回到上一级目录,去寻找test.txt,很显然,上级目录不存在test.txt,所以报错
创建的软连接,指向的文件,默认会以软链接的路径为主,去寻找指向的文件,所以创建时,请以软链接的路径作为起点路径 去写原文件的相对路径
而由于软链接 inode指向的数据块只保存 原文件的地址字符串,所以可以跨分区、跨设备创建,并且文件夹也可以创建。当然如果原文件被删除了,链接则也会失效
5、Linux下的文件类型的
- 普通文件,硬链接也是普通文件,文件标识符为-
- 目录,文件标识符为d
- 软链接(符号链接),文件标识符为l
- 面向块的设备文件,例如磁盘,驱动器、网卡,文件标识符为b
- 面向字符的设备文件,例如/dev/下的tty0、tty1、tty2等,它们就对应着键盘的每个字符,文件表示符为c
- 管道(匿名管道)和命名管道,文件标识符为p
- socket,文件标识符为s
即使是磁盘,驱动器,网卡,显示器这样的硬件设备,在Linux下也将其抽象成文件,这就符合Linux下一切皆文件的概念