天朗气清,惠风和煦,今日无事,遂来更新。
1.概述
总所周知,我们存的数据都是在一个叫硬盘的东西里面,这个硬盘又像个黑盒,这章就来简单解析一下Linux中文件系统。
现在我们用的大都是固态硬盘,除了服务器级别的主机在用机械硬盘,已经很少有人用机械硬盘啦,我们今天就要从机械硬盘讲起。因为Linux中的Ext3、Ext4基本是Ext2文件系统的增强版,所以本章以Ext2文件系统为例。
硬盘里有多层的磁盘和多个磁头,早期的磁盘使用的是CHS(磁道(柱面)、磁头、扇面)定位寻址,而系统需要将CHS地址转换为LBA(Local Block Adress)地址来使用,硬盘就可以看作是一个三维数组,CHS转LBA,就可以看作是将三维数组转化为一维数组。
系统读取硬盘的基本单位一般是“块(block)”,块是又n个扇区组成的,系统可以给硬盘分区,分区的最小单位是柱面,柱面内包含各个扇区,即包含了各个块。操作系统为了管理这些分区,引入Ext文件系统,Ext文件系统将各个分区以组为单位分为了n个块组(Block Group),每个块组内记录着本区域内的各种数据,其中就包括了每个存储在此区域文件的inode编号,通过inode以及其中各种位图就可以找到存储文件的属性以及数据,其中包括有各种目录与文件。
系统启动时,会自动挂载各个已经预先准备好的分区,只有挂载后分区才可用。
Linux中的目录也是文件,属性(元数据)与一般文件区别不大,数据存储的是目录名与目录下存储的各种文件的inode的映射关系,可以将目录看作一个一维数组,里面可能存有别的目录也可能是文件。
访问文件时,其实是打开当前工作目录通过文件名找到对应的inode,去查找文件内容,因此系统要解析文件路径,通过在内核中维护的树状路径结构的结构体struct dentry去查找文件,如果找到了就会返回文件的属性inode和内容,如果没找到就会将文件的struct dentry添加到内核缓冲中(每个文件都有对应的dentry结构)。
Linux中还有软链接和硬链接,软链接有点像Windows中的快捷访问方式,硬链接相当备份,原理是引用计数。
2.硬盘与CHS和LBA
这里用一张简单的硬盘剖面图来展开:
【计算机组成原理】快到碗里来,轻松图解硬盘结构~_磁盘结构-CSDN博客
硬盘内有磁头(Head)、磁道(track)、扇区(sector),硬盘通过先定位磁头,再确认访问某个磁道(柱面),最后定位扇区来进行定位。扇区从磁盘中读出和写入信息的最小单位一般是512字节。
我们知道文件 = 属性 + 内容,对于硬盘而言就是多定位几个扇区的事。
硬盘的各种属性:
磁头数:一般每个磁盘的上下两面都会有一个磁头。(传动臂上的磁头是共进退的)
磁道数:从0开始编号,从外往内计数,最靠进轴心和最靠外用于停放磁头的磁道不用作存储数据。
柱面数:由同一柱面上的磁道构成,数量上与一个磁盘盘面的磁道数相同。
扇区数:每个磁道被切分为多个扇区,每个磁道的扇区相同。
磁盘数:盘数。
磁盘容量 = 磁头数 x 磁道(柱面)数 x 每磁道扇区数 x 每扇区字节数。
硬盘是:由多个柱面构成
2.1CHS与LBA的转换
所以整张硬盘就像是一个三维数组(蛋卷-不是),在寻找某个扇区时,就要先寻找某个柱面,再确定在哪个磁道(其实是磁头),最后再确定扇区,由此CHS寻址就产生了。
我们在C/C++中学过数组,二维数组的下标通过算法转换其实是可以转换成一维数组的,所以CHS转换成LBA其实同理,Belike:
这样CHS就转换为了LBA线性地址。
那么谁来做这个转换工作呢,当然是由硬盘(硬件电路,伺服系统)自己来做。
CHS转成LBA:
单个柱⾯的扇区总数 = 磁头数 * 每磁道扇区。
LBA = 柱⾯号C * 单个柱⾯的扇区总数 + 磁头号H * 每磁道扇区数 + 扇区号S - 1。
即:LBA = 柱⾯号C * (磁头数 * 每磁道扇区数) + 磁头号H * 每磁道扇区数 + 扇区号S - 1。
扇区号通常是从1开始的,⽽在LBA中,地址是从0开始的。
柱⾯和磁道都是从0开始编号的。
总柱⾯,磁道个数,扇区总数等信息,在磁盘内部会⾃动维护,上层开机的时候,会获取到这些参 数。
LBA转成CHS:"//": 表⽰除取
柱⾯号C = LBA // (磁头数*每磁道扇区数,就是单个柱⾯的扇区总数)。
磁头号H = (LBA % (磁头数*每磁道扇区数)) // 每磁道扇区数。
扇区号S = (LBA % 每磁道扇区数) + 1。
2.2有关硬盘的各个属性
2.2.1“块”
操作系统在读取硬盘(典型的“块”设备)数据的时候,并不会一个一个扇区的读取,而是一次读取多个扇区,就是以“块”为单位进行读取,硬盘的分区被分为各个块,块的大小是在格式化的时候设置的,并非不可改变的,通常来说由8个扇区组成一个块,即一个块 = 4096字节(4KB)。
硬盘我们可以看作是一个三维数组,把他转换为一维数组后,数组下标就是LBA,每一个扇区都LBA。
已知LBA:块号 = LBA / 8 (整除)。
已知块:LBA = 块号 * 8 + n(块内第几个扇区)。
那么块是怎么划分的呢?我们后面讲。
2.2.2分区
硬盘的分区,其实就是给硬盘进行格式化,柱面是分区的最小单位(为了保证物理连续性,减少碎片化)。
2.2.3“inode”
我们都知道文件 = 属性 + 内容,并且我们目前还知道文件的内容存在块中,为什么还要有inode呢?
其实inode叫做“索引节点”,用于指向文件的属性信息,inode属性信息大小都是一样,通常为128字节或256字节。并且在Linux中inode作为文件的唯一标识。我们来看看inode长什么样↓
指令为 ls -li
第一个属性就是inode,第二个为模式,第三个为硬链接数,第四个为文件所有者,第五个为用户组,第六个为文件大小(字节),第七个为最后修改的日期,最后的为文件名。
下面为源码:
struct ext2_inode {
__le16 i_mode; /* File mode */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i_size; /* Size in bytes */
__le32 i_atime; /* Access time */
__le32 i_ctime; /* Creation 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; /* Blocks count */
__le32 i_flags; /* File flags */
union {
struct {
__le32 l_i_reserved1;
} linux1;
struct {
__le32 h_i_translator;
} hurd1;
struct {
__le32 m_i_reserved1;
} masix1;
} osd1; /* OS dependent 1 */
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
__le32 i_generation; /* File version (for NFS) */
__le32 i_file_acl; /* File ACL */
__le32 i_dir_acl; /* Directory ACL */
__le32 i_faddr; /* Fragment address */
union {
struct {
__u8 l_i_frag; /* Fragment number */
__u8 l_i_fsize; /* Fragment size */
__u16 i_pad1;
__le16 l_i_uid_high; /* these 2 fields */
__le16 l_i_gid_high; /* were reserved2[0] */
__u32 l_i_reserved2;
} linux2;
struct {
__u8 h_i_frag; /* Fragment number */
__u8 h_i_fsize; /* Fragment size */
__le16 h_i_mode_high;
__le16 h_i_uid_high;
__le16 h_i_gid_high;
__le32 h_i_author;
} hurd2;
struct {
__u8 m_i_frag; /* Fragment number */
__u8 m_i_fsize; /* Fragment size */
__u16 m_pad1;
__u32 m_i_reserved2[2];
} masix2;
} osd2; /* OS dependent 2 */
};
...
#define EXT2_NDIR_BLOCKS 12
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)
3.Ext2文件系统
我们上述的文件内容,基本都是由文件系统来管理的,要用硬盘存储数据的话,首先要先把硬盘格式化成某个文件系统的格式,然后才能使用,文件系统存在的目的就是组织和管理硬盘中的数据和文件。
在Linux中最常见的文件系统就是Ext2文件系统,Ext3和Ext4基本是Ext2文件系统的增强版,其核心基本没有变化。
Ext文件把整个分区分为n个大小相同的块组,如下图↓
与EXT2 File System一起的是Boot Sector(启动块),其大小是确定的1KB,根据PC标准规定,它储存着磁盘分区信息和启动信息,任何文件系统都是无法修改启动块的。
3.1Block Group
我们可以看到Block Group内部分为 Super Block、GDT、Block Bitmap、inode Bitmap、inode Table、Data Blocks。
3.1.1Super Block
Super Block内存储着文件系统本身的结构和信息,描述了整个分区的文件系统信息。记录的信息主要有:bolck和inode的总量,未使⽤的block和inode的数量,⼀个block和inode的⼤⼩,最近⼀次挂载的时间,最近⼀次写 ⼊数据的时间,最近⼀次检验磁盘的时间等其他⽂件系统的相关信息。如果Super Block被破坏了,整个文件系统就被破坏了。
Super Block在每个组块内都有备份(也有可能是第一个块组有,其他块组可以没有)。系统对Super Block有多个备份,就是为了磁盘部分出现物理问题时文件系统也能正常工作,保证文件系统的Super Block信息可以正常访问。而且每个Super Block存储的数据都相同。
struct ext2_super_block {
__le32 s_inodes_count; /* Inodes count */ √
__le32 s_blocks_count; /* Blocks count */
__le32 s_r_blocks_count; /* Reserved blocks count */ √
__le32 s_free_blocks_count; /* Free blocks count */ √
__le32 s_free_inodes_count; /* Free inodes count */ √
__le32 s_first_data_block; /* First Data Block */ √
__le32 s_log_block_size; /* Block size */
__le32 s_log_frag_size; /* Fragment size */
__le32 s_blocks_per_group; /* # Blocks per group */ √
__le32 s_frags_per_group; /* # Fragments per group */
__le32 s_inodes_per_group; /* # Inodes per group */ √
__le32 s_mtime; /* Mount time */
__le32 s_wtime; /* Write time */
__le16 s_mnt_count; /* Mount count */
__le16 s_max_mnt_count; /* Maximal mount count */
__le16 s_magic; /* Magic signature */
__le16 s_state; /* File system state */
__le16 s_errors; /* Behaviour when detecting errors */
__le16 s_minor_rev_level; /* minor revision level */
__le32 s_lastcheck; /* time of last check */
__le32 s_checkinterval; /* max. time between checks */
__le32 s_creator_os; /* OS */
__le32 s_rev_level; /* Revision level */
__le16 s_def_resuid; /* Default uid for reserved blocks */
__le16 s_def_resgid; /* Default gid for reserved blocks */
/*
* These fields are for EXT2_DYNAMIC_REV superblocks only.
*
* Note: the difference between the compatible feature set and
* the incompatible feature set is that if there is a bit set
* in the incompatible feature set that the kernel doesn't
* know about, it should refuse to mount the filesystem.
*
* e2fsck's requirements are more strict; if it doesn't know
* about a feature in either the compatible or incompatible
* feature set, it must abort and not try to meddle with
* things it doesn't understand...
*/
__le32 s_first_ino; /* First non-reserved inode */
__le16 s_inode_size; /* size of inode structure */ √
__le16 s_block_group_nr; /* block group # of this superblock */
__le32 s_feature_compat; /* compatible feature set */
__le32 s_feature_incompat; /* incompatible feature set */
__le32 s_feature_ro_compat; /* readonly-compatible feature set */
__u8 s_uuid[16]; /* 128-bit uuid for volume */
char s_volume_name[16]; /* volume name */
char s_last_mounted[64]; /* directory where last mounted */
__le32 s_algorithm_usage_bitmap; /* For compression */
/*
* Performance hints. Directory preallocation should only
* happen if the EXT2_COMPAT_PREALLOC flag is on.
*/
__u8 s_prealloc_blocks; /* Nr of blocks to try to preallocate*/
__u8 s_prealloc_dir_blocks; /* Nr to preallocate for dirs */
__u16 s_padding1;
/*
* Journaling support valid if EXT3_FEATURE_COMPAT_HAS_JOURNAL set.
*/
__u8 s_journal_uuid[16]; /* uuid of journal superblock */
__u32 s_journal_inum; /* inode number of journal file */
__u32 s_journal_dev; /* device number of journal file */
__u32 s_last_orphan; /* start of list of inodes to delete */
__u32 s_hash_seed[4]; /* HTREE hash seed */
__u8 s_def_hash_version; /* Default hash version to use */
__u8 s_reserved_char_pad;
__u16 s_reserved_word_pad;
__le32 s_default_mount_opts;
__le32 s_first_meta_bg; /* First metablock block group */
__u32 s_reserved[190]; /* Padding to the end of the block */
};
3.1.2GDT
GDT(块组描述符表),描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描 述符存储⼀个块组的描述信息,如在这个块组中从哪⾥开始是inodeTable,从哪⾥开始是Data Blocks,空闲的inode和数据块还有多少个等等。块组描述符在每个块组的开头都有⼀份拷⻉。
struct ext2_group_desc
{
__le32 bg_block_bitmap; /* Blocks bitmap block */
__le32 bg_inode_bitmap; /* Inodes bitmap block */
__le32 bg_inode_table; /* Inodes table block */
__le16 bg_free_blocks_count; /* Free blocks count */
__le16 bg_free_inodes_count; /* Free inodes count */
__le16 bg_used_dirs_count; /* Directories count */
__le16 bg_pad;
__le32 bg_reserved[3];
};
3.1.3Block Bitmap
块位图,记录着Data Block中每个块的占用信息,可以知道哪个块被占用。
3.1.4Inode Bitmap
inode位图中,每个bit表示inode是否空闲可用。
3.1.5Inode Table
inode表,当前组的所有inode集合,存放着文件属性,最近修改日期,所属者等等。inode标号以分区为单位整体划分,不可越区访问。
3.1.6Data Block
数据区,存放着一个个Block:存放⽂件内容。
根据不同的⽂件类型有以下⼏种情况:
对于普通⽂件,⽂件的数据存储在数据块中。
对于⽬录,该⽬录下的所有⽂件名和⽬录名存储在所在⽬录的数据块中,除了⽂件名外,ls -l命令 看到的其它信息保存在该⽂件的inode中。
3.2inode与Data Block的(简化)映射
inode内部存在
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
EXT2_N_BLOCKS = 15 ,就是⽤来进⾏inode和block映射的
因此创建⼀个新⽂件主要有以下4个操作:(以图为例)----找的网图
1.存储属性:内核先找到一个空闲的inode节点,内核把文件属性记录到inode中,这里的inode为263466。
2.存储数据:该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。
3.记录分配情况:文件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表(GDT)。
4. 添加文件名到目录:新的文件名abc。linux如何在当前的目录中记录这个文件?内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
3.3目录与路径解析
我们都说,一切皆文件,那么目录是文件吗,怎么理解目录。
实际上目录也是文件,它也是属性 + 内容,不过他的内容存放的是它目录下的目录以及文件的映射关系,也就是各个文件名与inode的映射关系。
简单抽一段代码看看↓
1 #include <stdio.h>
2 #include <string.h>
3 #include <stdlib.h>
4 #include <dirent.h>
5 #include <sys/types.h>
6 #include <unistd.h>
7 int main(int argc, char *argv[]) {
8 if (argc != 2) {
9 fprintf(stderr, "Usage: %s <directory>\n", argv[0]);
10 exit(EXIT_FAILURE);
11 }
12 DIR *dir = opendir(argv[1]); // ϵͳµ÷Óã¬×ÔÐвéÔÄ
13 if (!dir) {
14 perror("opendir");
15 exit(EXIT_FAILURE);
16 }
17 struct dirent *entry;
18 while ((entry = readdir(dir)) != NULL) { // ϵͳµ÷Óã¬×ÔÐвéÔÄ
19 if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..")== 0)
20 {
21 continue;
22 }
23 printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned
24 long)entry->d_ino);
25 }
26 closedir(dir);
27 return 0;
28 }
我们可以看到,打开目录就要获得对应的文件名,找到对应的inode号,然后访问目录(文件),本质上是我们必须能打开当前工作目录文件,查看目录文件内容。
这时我们不禁要问,当前我们所在的目录,不是工作目录吗?当前的目录不就打开文件名进来吗,访问不是需要inode吗?我们怎么打开。
Linux中要打开我们当前工作目录,就要有一个层层“递归”的过程,我们要递归到最上层,也就是 "/",我们也叫它根目录,每次访问文件,我们都要从根目录开始,依次打开每一个文件目录,知道找到我们要打开文件的文件名。这个过程就是路径解析。(访问文件: 目录 + 文件名 = 路径)
并且在Linux中访问文件,本质上是由进程进行访问,而进程PCB有CWD(进程的当前工作目录),而我们每次进入目录,打开文件都会被记录,也就有了路径。
而根目录是Linux系统提供的目录文件,所以Linux的路径结构是由系统和用户一起构建的。
那么问题来了,每次都由根目录打开是否过于慢、低效了。
3.4路径缓存
如果每次都从根目录开始,那确实太低速了。
在Linux系统中,OS会自己缓存历史路径结构,OS会自动维护近期打开的路径结构。
Linux在内核中维护的树状路径结构结构体为:struct dentry
struct dentry {
/* RCU lookup touched fields */
unsigned int d_flags; /* protected by d_lock */
seqcount_t d_seq; /* per dentry seqlock */
struct hlist_bl_node d_hash; /* lookup hash list */
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
struct inode *d_inode; /* Where the name belongs to - NULL is
* negative */
unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */
/* Ref lookup also touches following */
struct lockref d_lockref; /* per-dentry lock and refcount */
const struct dentry_operations *d_op;
struct super_block *d_sb; /* The root of the dentry tree */
unsigned long d_time; /* used by d_revalidate */
void *d_fsdata; /* fs-specific data */
union {
struct list_head d_lru; /* LRU list */
wait_queue_head_t *d_wait; /* in-lookup ones only */
};
struct list_head d_child; /* child of parent list */
struct list_head d_subdirs; /* our children */
/*
* d_alias and d_rcu can share memory
*/
union {
struct hlist_node d_alias; /* inode alias list */
struct hlist_bl_node d_in_lookup_hash; /* only for in-lookup ones */
struct rcu_head d_rcu;
} d_u;
} __randomize_layout;
注:
每个⽂件其实都要有对应的dentry结构,包括普通⽂件。这样所有被打开的⽂件,就可以在内存中 形成整个树形结构
整个树形节点也同时会⾪属于LRU(LeastRecentlyUsed,最近最少使⽤)结构中,进⾏节点淘汰
整个树形节点也同时会⾪属于Hash,⽅便快速查找
更重要的是,这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何⽂件,都在先在这 棵树下根据路径进⾏查找,找到就返回属性inode和内容,没找到就从磁盘加载路径,添加dentry 结构,缓存新路径。
3.5小结
分区之后的格式化操作,就是对分区进⾏分组,在每个分组中写⼊SB、GDT、Block Bitmap、InodeBitmap等管理信息,这些管理信息统称:⽂件系统。
只要知道⽂件的inode号,就能在指定分区中确定是哪⼀个分组,进⽽在哪⼀个分组确定 是哪⼀个inode,进而⽂件属性和内容就全部都有了。
放几个网图帮组理解:
4. 软硬连接
硬链接是通过inode引用另外一个文件,软链接是通过名字引用另外一个文件,但实际上,新的文件和被引用的文件的inode不同,应用常见上可以想象成一个类似windows的快捷方式
1.硬链接
指令:ln 源文件 链接创建的文件名
我们可以看到硬链接文件的属性几乎是一至的。
. 和 .. 都是硬链接
2.软链接
指令:ln -s 源文件 链接创建的文件名
我们可以看到软链接文件有一个指向性的箭头。
祝大家技术更加精进,文章若有错误请指出。