目录
3.3.2 GDT(Group Descriptor Table)
1. 理解硬件
1.1 磁盘、服务器、机柜、机房
- 机械磁盘是计算机中唯一的一个机械设备
- 磁盘 ---外设
- 慢
- 容量大,价格便宜
1.2 磁盘物理结构
- 主轴:它是磁盘的中心部件,电机带动主轴旋转,从而让磁盘以很高的速度匀速转动。像在硬盘中,其转速较高,常见的有5400转/分钟、7200转/分钟,这使得磁头能够在盘片高速旋转时快速定位和读写数据。
- 磁头臂:主要起到支撑和移动磁头的作用。它能根据系统指令灵活地将磁头精确地移动到磁盘的特定位置,就好像是一个精准的搬运工具,确保磁头可以到达盘片上任何需要进行数据读写的磁道。
- 永磁铁:在磁盘系统里主要用于产生磁场。这种磁场能够为磁头读写数据提供稳定的磁场环境,使得磁头可以准确地感知磁盘上磁性物质的磁极变化,同时在写入数据时,辅助磁头产生足够的磁场强度来改变磁盘磁性涂层的磁极状态。
- 磁盘:是存储数据的核心部件,一般是圆形的盘片,其表面涂有磁性材料。数据以二进制的形式,通过磁头改变磁盘磁性涂层的磁极方向来存储在磁盘上。在主轴的带动下旋转,使得磁头可以扫描磁盘不同位置,从而读取或写入数据。
- 磁头:是实现数据读写的关键部件。读取数据时,磁头会检测磁盘表面磁性物质的磁极方向,将其转换为电信号,从而获取数据。写入数据时,磁头会产生磁场,按照一定的编码规则改变磁盘磁性涂层的磁极方向,把数据存储到磁盘上。
磁头在摆动的本质: 定位磁道(柱面)
磁盘盘片旋转的本质: 定位扇区
1.3 磁盘的存储结构
扇区: 是磁盘存储数据的基本单位,512字节,块设备
如何定位一个扇区呢?
- 可以先定位磁头(header)
- 确定磁头要访问哪一个柱面(磁道)(cylinder)
- 定位一个扇区(sector)
- CHS 地址定位
文件 = 内容 + 属性 都是数据,无非就是占据哪几个扇区的问题! 能定位一个扇区了,能不能定位多个扇区呢?
- 扇区是从磁盘读出和写入信息的最小单位,通常大小为512字节。
- 磁头(head)数:每个盘片一般有上下两面,分别对应1个磁头,共两个磁头
- 磁道(track)数:磁道是从盘片外围往内圈编号0磁道,1磁道.... 靠近主轴的同心圆用于停靠磁头,不存储数据
- 柱面(cylinder)数:磁道构成柱面,数量上等同于磁道个数
- 扇区(sector)数:每个磁道都被切分成很多扇形区域,每道的扇区数量相同
- 圆盘(platter)数:就是盘片的数量
- 磁盘容量= 磁头数*磁道(柱面)数* 每道扇区数* 每扇区字节数
- 细节:传动臂上的磁头是共进退的(这点比较重要,后面会说明)
知道了 柱面(cylinder),磁头(head),扇区(sector),显然可以定位数据了,这就是数据定位(寻址)方式之一,CHS寻址方式。
CHS寻址:
对早期的磁盘非常有效,知道用那个刺头,读取哪个柱面上的第几扇区就可以读到数据了。但是CHS模式支持的硬盘容量有限,因为系统用8bit来存储刺头地址,用10bit来存储煮面地址,用6bit来存储扇区地址,而一个扇区共有512byte,这样使用CHS寻址一块磁盘最大容量为256*1024*63*512B =8064 MB(1MB = 1048576B)(若按1MB =1000000B来算就是8.4GB)
1.4 磁盘的逻辑结构
1.4.1 理解过程
磁带上面可以存储数据,我们可以把磁带“拉直”,这样就形成了线性结构
那么磁盘本质上虽然是硬质的,但是逻辑上我们可以把磁盘想象成卷在一起的磁带,那么磁盘的逻辑存储结构我们也可以类似于:
这样每一个扇区,就有了一个线性地址(其实就是数组下标,这种地址叫做LBA)
1.4.2 真实过程
一个细节:传动臂上的磁头是共进退的。
柱面是一个逻辑上的概念,其实就是每一个面上,相同半径的磁道逻辑上构成柱面。
所以,磁盘物理上分了很多面,但是在我们看来,逻辑上,磁盘整体是由“柱面”卷起来的。
类似于这样
所以,磁盘的真实情况是:
磁道:
某一盘面的某一个磁道展开:
即:一维数组
柱面:
整个磁盘所有盘面的同一个磁道,即柱面展开:
- 柱面上的每个磁道,扇区个数是一样的
- 这不就是二维数组吗?
整盘:
整个磁盘不就是多张二维的扇区数组表(三维数组?)
所有,寻址一个扇区:先找到哪一个柱面(Cylinder),在确定柱面内哪一个磁道(其实就是磁头位置,Head),在确定扇区(Sector),所以就有了CHS。
我们之前学过C/C++的数组,在外面看来,其实全部都是一维数组:
所以,每一个扇区都有一个下标,我们叫做LBA(Logical Black Address) 地址,其实就是线性地址。所以怎么计算得到这个LBA地址呢?
所以,我们接下来就需要思考,如何将LBA地址转成CHS地址,CHS地址如何转换成为LBA地址。
OS只需要使用LBA 就可以了!! 那么 LBA地址转成CHS地址,CHS如何转换成为LBA地址。谁做啊?? 磁盘自己来做! 固件(硬件电路,伺服系统)
1.5 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
- “//” 表示除取整
从此往后,在磁盘使用者看来,根本就不需要关心CHS地址,而是直接使用LBA地址,磁盘内部自己转换。
所以, 从现在开始,磁盘就是一个 元素为扇区 的一维数组,数组的下标就是每一个扇区的LBA地址。OS 使用磁盘,就可以用一个数字访问磁盘扇区了。
2. 引入文件系统
2.1 引入“块”概念
其实硬盘就是典型的“块”设备,操作系统读取硬盘数据的时候,其实不会一个一个扇区读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个‘块’(block)。
硬盘的每个分区是被划分为一个个的“块”。一个“块”的大小是由格式化的时候确定的,并且不可以改变,最常见的是4KB,即连续八个扇区组成一个“块”。“块”是文件存取的最小单位、
注意:
- 磁盘就是一个三维数组,我们把它看待成为一个“一维数组”,数组下标就是LBA,每个元素都是扇区。
- 每个扇区都有LBA,那么8个扇区一个块,每一个块的地址我们也能算出来。
- 知道LBA:块号=LBA / 8
- 知道块号: LBA = 块号*8 + n(n是块内第几个扇区)
2.2 引入 “分区”概念
其实磁盘是可以被分成多个分区(partition)的,以Windows观点来看,你可能会有一块磁盘并且将它分区成C,D,E盘。那么C,D,E就是分区。分区从实质上说就是对硬盘的一种格式化。但是Linux 的设备都是以文件形式存在,那是怎么分区的呢?
柱面是分区的最小单位,我们可以利用参考柱面号码的方式来进行分区,其本质就是设置每个区的起始柱面和结束柱面号码。此时我们可以将硬盘上的柱面(分区)进行平铺,将其想象成一个大的平面。如下图所示:
注意:
柱面大小一致,扇区个位一致,那么其实只要知道每个分区的起始和结束柱面号,知道每一个柱面多少个扇区,那么该分区多大,其实和解释LBA是多少也就清除了。
2.3 引入 "inode"概念
之前我们说过 文件 = 数据 + 属性, 我们使用 ls -l 的时候看到的除了看到文件名,还能看到文件元数据(属性)。
[root@localhost linux]# ls -l
总⽤量 12
-rwxr-xr-x. 1 root root 7438 "9⽉ 13 14:56" a.out
-rw-r--r--. 1 root root 654 "9⽉ 13 14:56" test.c
每行包含7列:
- 模式
- 硬链接数
- 文件所有者
- 组
- 大小
- 最后修改时间
- 文件名
ls -l 读取存储在磁盘上的文件信息,然后显示出来
其实这个信息,除了通过这种方式来获取,还有一个stat命令能够看到更多信息
root@localhost linux]# stat test.c
File: "test.c"
Size: 654 Blocks: 8 IO Block: 4096 普通⽂件
Device: 802h/2050d Inode: 263715 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2017-09-13 14:56:57.059012947 +0800
Modify: 2017-09-13 14:56:40.067012944 +0800
Change: 2017-09-13 14:56:40.069012948 +0800
到这里我们要思考一个问题,文件数据都存储在“块” 中,那么很显然,我们还必须找到一个地方存储文件的元信息(属性信息),比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为“索引节点”。
每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。为了能解释清楚inode,我们需要深入了解一下文件系统。
注意:
- Linux下文件的存储是属性和内容分离存储的
- Linux下,保存文件属性的集合叫做inode,一个文件,一个inode,inode内有一个唯一的标识符,叫做inode号
- 删除的本质就是设置inode,block无效!
那么一个文件的属性inode到底长什么样子呢?
/*
* Structure of an inode on the disk
*/
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 */
};
/*
* Constants relative to the data blocks
*/
#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)
备注:EXT2_N_BLOCKS = 15
再次注意:
- 文件名属性并未纳入到inode数据结构内部
- inode 的大小一般是128字节或者256字节,我们后面统一128字节
- 任何文件的内容大小可以不同,但是属性大小一定是相同的
到目前为止,相信大家还有两个问题:
- 我们已经知道硬盘是 典型的“块”设备,操作系统读取硬盘数据的时候,读取的基本单位是“块”。“块”又是硬盘的每个分区下的结构,难道“块”是随意的在分区上排布的吗? 那要怎么找到“块”呢?
- 还有就是上面提到的存储文件属性的inode,又是如何放置的呢?
文件系统就是为了组织管理这些的!!
注: 格式化实际就是写入空的文件系统!
3. ext2 文件系统
3.1 宏观认识
所有的准备工作都已经做完了,是时候认识下文件系统了。我们想要在硬盘上存储文件,必须先把硬盘格式化为某种格式的文件系统,才能存储文件。文件系统的目的就是组织和管理硬盘中的文件。在Linux 系统中,最常见的是ext2 系列的文件系统。其最早期版本ext2,后面又发展出ext3和ext4。ext3和ext4 虽然对ext2 进行了增强,但是其核心设计并没有发生变化,我们仍是以较老的ext2 作为演示对象。
ext2 文件系统将整个分区划分为若干个同样大小的块组(Block Group), 如下图所示。只要能管理一个分区就能管理所有分区,也就能管理所有磁盘文件。
上图中启动块(Boot Block/Sector)的大小是确定的,为1KB,由PC标准规定,用来存储磁盘分区信息和启动信息,任何文件系统都不能修改启动块。启动块之后才是ext2文件系统的开始。
注: 通过上图,我们也发现了,各个分区都是单独各自管理的,互不干涉,所以各个分区独自使用各自的文件系统(即磁盘中可以使用不同的文件系统,一个分区可以独自使用一种文件系统)
3.2 Block Group
ext2文件系统会根据分区的大小划分为数个Block Group。而每个Bloack Group都有相同的结构组成。
3.3 块组内部构成
3.3.1 超级快(Super Block)
存放文件系统本身的系统信息,描述整个分区的文件系统信息。记录的信息主要有:block 和inode 的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block 的信息被破坏,可以说整个文件系统结构就被破坏了!!
超级块在每个块组的开头都有一份拷贝(第一个块组必须有,后面的块组可以没有)。为了保证文件系统在磁盘部分扇区出现物理问题的情况下还能正常工作,就必须保证文件系统的super block信息在这种情况下也能正常访问。所以一个文件系统的super block 会在多个block group中进行备份,这些super block区域的数据保持一致。
/*
* Structure of the 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.3.2 GDT(Group Descriptor Table)
块组描述符表,描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储一个块组的描述信息,如在这个块组中从哪里开始是inode Table,从哪里开始是Data Blocks,空闲的inode和数据块还有多少个等等。块组描述符在每个块组的开头都有一份拷贝。
// 磁盘级blockgroup的数据结构
/*
* Structure of a blocks group descriptor
*/
struct ext2_group_desc
{
__le32 bg_block_bitmap; /* Blocks bitmap block */
__le32 bg_inode_bitmap; /* Inodes bitmap */
__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.3.3 块位图(Block Bitmap)
- Block Bitmap 中记录着 Data Block中哪个数据块已经被占用,哪个数据块没有被占用
3.3.4 inode位图(Inode Bitmap)
- 每个bit表示一个inode是否空闲可用。
3.3.5 i节点表(Inode Table)
- 存放文件属性 如 文件大小, 所有者,最近修改时间等
- 当前分组所有Inode属性的集合
- inode编号以分区为单位,整体划分,不可跨分区
3.3.6 Data Block
数据区: 存放文件内容,也就是一个一个的Block。根据不同的文件类型有以下几种情况:
- 对于普通文件,文件的数据存储在数据块中。
- 对于目录,该目录下的所有文件名和目录名存储在所有目录的数据块中,除了文件名外,ls -l命令看到的其他信息保存在该文件的inode中。
- Block号按照分区划分,不可跨分区
3.4 inode 和datablock 映射(弱化)
- inode内部存在 __le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */ , EXT2_N_BLOCKS =15, 就是用来进行inode和block映射的
- 对于小文件(文件大小小于或等于一个数据块大小),inode中的指针直接指向存储文件内容的数据块。
- 对于大文件,inode中有直接块指针、间接块指针、二次间接块指针和三次间接块指针。直接块指针可以直接指向文件开始部分的数据块。间接块指针指向的块里存储的是其他数据块的地址,通过这种方式可以扩展能够访问的数据块范围,从而支持大文件存储。二次间接块和三次间接块是更高级的扩展方式,能够支持非常大的文件存储,原理是通过更多层次的间接指向来获取数据块。
- 这样文件=内容 + 属性,就都能找到了。
这里补充几点
关于inode
1 . inode 以分区为单位,一套inode
2. inode 分配的时候,只需要确定起始inode即可。因为每一个块组分配inode只需要当前块组的起始inode + 每个块组的inode总数,就可以得出当前块组inode的范围
关于block
块号也是统一进行编号
那么这里我们思考两个问题
1. 我们是如何分配一个inode的?
上面我们说过,inode分配的时候,只需要确定起始inode即可,那么我们怎么确定各个块组的inode范围呢? 我们可以通过这个公式计算,设块组号为n,每个块组的inode数量为m,那么这个块组的inode范围就是从(n-1)*m +1 到 n*m。
当我们要分配一个inode,大致分为5步:
1.文件创建时,当我们要分配一个inode时,大概率也就是我们创建了一个文件,所以,文件系统首先会检查是否有空闲的inode。他会通过inode位图(inode bitmap)来查找,位图中的每一位对应一个inode,0为空闲,1为已使用。
2. 定位空闲inode: 从inode位图开头开始扫描,找到第一个值为0的位置,这个位置对应的inode就是我们要分配的空闲inode,inode = 起始inode+inode 位图的第几位(从0开始)
3. 更新inode位图: 将找到的空闲inode在位途中对应的位设为,表示该inode已被占用。
4. 初始化inode信息:将新文件的基本信息写入这个分配好的inode。包括文件类型(如普通文件、目录等)、权限、所有者、所属组、文件大小(初始为0)和时间戳(创建时间、访问时间、修改时间等)。
5. 关联数据块(如果有):如果文件创建时就有文件写入,文件系统会文件数据分配数据快,并将inode中的指针指向这些数据快,建立起文件数据与inode 的关联。
2.我们是如何分配一个block呢?
其实大致步骤与上面分配inode相同 都是 先挨个扫描查找第一个空闲位置,然后更新位图,然后关联到对应位置。
那么,我们在深入思考下,在已知inode号的情况下,在指定分区,对文件进行增、删、查、改本质上是在做什么?
增: 本质上就是在分配一个inode,步骤和上面一样,这里就不赘述了。
删: 实质就是将该inode所对应的位图由1改为0。
查: 实际就是调取当前inode所关联的block
改: 实际就是读取当前inode所关联的block,修改完毕之后,在写回去
那么,如果在未知inode的情况下,文件系统,是如何实现查找一个文件的呢?
这个我们后面再说。
结论:
- 分区之后的格式化操作,就是对分区进行分组,在每个分组中写入SB、GDT、Block Bitmap、Inode Bitmap等管理信息,这些管理信息统称:文件系统
- 只要知道文件的inode号,就能在指定分区中确定是哪个分组,进而在哪一个分组确定是哪一个inode
- 拿到inode文件,属性和内容就全部都有了。
下面,通过touch一个新文件来看看文件系统是如何工作。
1 [root@localhost linux]# touch abc
2 [root@localhost linux]# ls -i abc
3 263466 abc
为了说明问题,我们将上图简化:
创建一个新文件主要有以下4个操作
1.存储属性
内核先找到一个空闲的i节点(这里是263466)。内核把文件信息记录到其中。
2. 存储数据
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第一块数据复制到300,下一块复制到500,以此类推。
3. 记录分配情况
文件内容按顺序300.500.800存放。内核在inode上的磁盘分布区记录了上述块列表。
4. 添加文件名到目录
新的文件名abc。Linux如何在当前的目录中记录这个文件? 内核将入口(263466,abc)添加到目录文件。文件名和inode之间的对应关系将文件名和文件的内容及属性连接起来。
3.5 目录与文件名
问题:
- 我们访问文件,都是用的文件名,没用过inode号啊?
- 目录是文件吗?如何理解?
答案:
- 目录也是文件,但是磁盘上没有目录的概念,只要文件属性 + 文件内容的概念。
- 目录的属性不用多说也就是inode属性结构,与普通文件不同的事,目录的内容保存的是:文件名和Inode号的映射关系。
// readdir.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <directory>\n", argv[0]);
exit(EXIT_FAILURE);
}
DIR *dir = opendir(argv[1]); // 系统调⽤,⾃⾏查阅
if (!dir) {
perror("opendir");
exit(EXIT_FAILURE);
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) { // 系统调⽤,⾃⾏查阅
// Skip the "." and ".." directory entries
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..")
== 0) {
continue;
}
printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned
long)entry->d_ino);
}
closedir(dir);
return 0;
}
上面 opendir函数用于打开一个目录,方便后续使用readdir等函数读取目录中的内容。它返回一个指向DIR 类型的指针,该指针只想一个内部数据结构,代表代开的目录六。
struct dirent 用于存储目录项(文件或子目录的相关信息)内容如下:
readdir 函数用于读取目录中的文件和子目录信息。它返回一个指向 struct dirent 类型的指针,当到达目录结尾或者出错时,返回NULL。如果返回NULL,可以使用error来确定是否到达目录末尾 (errno等于0为到达末尾,出现错误了,errno会有相应的错误码)
运行结果如下:
所以: 访问文件,必须打开当前目录,根据文件名,获得对应的inode号,然后进行文件访问。
所以,访问文件必须要知道当前工作目录,本质是必须能打开当前工作目录文件,查看目录文件的内容!
whb@bite:~/code/test/test$ pwd
/home/whb/code/test/test
whb@bite:~/code/test/test$ ls -li
total 24
1596260 -rw-rw-r-- 1 whb whb 814 Oct 28 20:32 test.c
⽐如:要访问test.c, 就必须打开test(当前⼯作⽬录),然后才能获取test.c对应的inode进⽽对⽂
件进⾏访问。
这里,我们就可以思考下目录权限的本质是什么?
- r读: 本质上就是能否访问目录文件的内容区,所以没有r权限,我们也就查看不了目录下的文件
- w写:本质上就是能否修改目录文件的内容区,所以没有w权限,我们也就无法添加新文件和修改文件属性
- x执行:本质上就是能否通过文件名访问所对应的inode。
3.6. 路径解析
问题:打开当前工作目录文件,查看当前工作目录文件的内容? 当前工作目录不也是文件吗?我们访问当前工作目录不也是只知道当前工作目录的文件名吗?要访问它,不也得知道当前工作目录的inode吗?
答案1: 所以也要打开:当前工作目录的上级目录,额.....,上级目录不也是目录吗?? 不还是上面的问题吗?
答案2: 所以类似 “递归”,需要把路径中所有的目录全部解析,出口是“/”根目录。
最终答案3:而实际上,任何文件,都有路径,访问目标文件,比如:
/home/yang/code/test/test/test.c
都要从根目录开始,依次打开每一个目录,根据目录名,依次访问每个目录下指定的目录,直到访问到test.c。这个过程叫做Linux路径解析。
注意:
- 所以,我们知道了:访问文件必须要有目录 + 文件名 = 路径的原因
- 根目录固定文件名,inode号,无需查找,系统开机之后就必须知道
可是路径谁提供?
- 你访问文件,都是指令/工具访问,本质是进程访问,进程有CWD!进程提供路径。
- 你open文件,提供了路径
可是最开始的路径从哪里来?
- 所以Linux为什么要有根目录,根目录下为什么要有那么多缺省目录?
- 你为什么要有家目录,你自己可以新建目录?
- 上面所有行为:本质就是在磁盘文件系统中,新建目录文件。而你新建的任何文件,都在你或者系统指定的目录下新建,这不天然就有路径了嘛!
- 系统+ 用户共同构建Linux路径结构。
3.7 路径缓存
问题1:Linux磁盘中,存在真正的目录吗?
答案:不存在,只有文件。只保存文件属性 + 文件内容
问题2:访问任何文件,都要从根目录开始进行路径解析吗?
答案:原则上是,但是这样太慢了,所以Linux会缓存历史路径结构
问题3:Linux目录的概念,怎么产生的?
答案:打开的文件是目录的话,由OS自己在内存中进行路径维护
而 Linux中,在内核中维护树状路径结构的内核结构体叫做: struct dentry
struct dentry {
atomic_t d_count;
unsigned int d_flags; /* protected by d_lock */
spinlock_t d_lock; /* per dentry lock */
struct inode *d_inode; /* Where the name belongs to - NULL is
* negative */
/*
* The next three fields are touched by __d_lookup. Place them here
* so they all fit in a cache line.
*/
struct hlist_node d_hash; /* lookup hash list */
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
struct list_head d_lru; /* LRU list */
/*
* d_child and d_rcu can share memory
*/
union {
struct list_head d_child; /* child of parent list */
struct rcu_head d_rcu;
} d_u;
struct list_head d_subdirs; /* our children */
struct list_head d_alias; /* inode alias list */
unsigned long d_time; /* used by d_revalidate */
struct dentry_operations *d_op;
struct super_block *d_sb; /* The root of the dentry tree */
void *d_fsdata; /* fs-specific data */
#ifdef CONFIG_PROFILING
struct dcookie_struct *d_cookie; /* cookie, if any */
#endif
int d_mounted;
unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */
};
注意:
- 每个文件其实都要有对应的dentry结构,包括普通文件。这样所有被打开的文件,就可以在内存中形成整个树形结构
- 整个树形节点也同时隶属于LRU(Least Recently Used,最近最少使用)结构中,进行节点淘汰
- 整个树形节点也同时会隶属于Hash,方便快速查找
- 更重要的是,这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何文件,都先在这棵树下根据路径进行查找,找到就返回属性inode和内容,没找到就从磁盘加载路径,添加dentry结构,缓存新路径。
那么,我们再思考下,文件描述符,进程,与文件系统的关联,如下图:
3.8 挂载分区
我们已经能够根据inode号在指定分区找文件了,也已经能根据目录文件内容,找指定的inode了,在指定的分区内,我们可以为所欲为了。可是:
问题:inode不是不能跨分区吗? Linux不是可以有多个分区吗? 我怎么知道我在哪一个分区??
3.8.1 一个实验:
dd if=/dev/zero of=./disk.img bs=1M count=5 #制作⼀个⼤的磁盘块,就当做⼀个分区
$ mkfs.ext4 disk.img # 格式化写⼊⽂件系统
$ mkdir /mnt/mydisk # 建⽴空⽬录
$ df -h # 查看可以使⽤的分区
Filesystem Size Used Avail Use% Mounted on
udev 956M 0 956M 0% /dev
tmpfs 198M 724K 197M 1% /run
/dev/vda1 50G 20G 28G 42% /
tmpfs 986M 0 986M 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 986M 0 986M 0% /sys/fs/cgroup
tmpfs 198M 0 198M 0% /run/user/0
tmpfs 198M 0 198M 0% /run/user/1002
$ sudo mount -t ext4 ./disk.img /mnt/mydisk/ # 将分区挂载到指定的⽬录
$ df -h
Filesystem Size Used Avail Use% Mounted on
udev 956M 0 956M 0% /dev
tmpfs 198M 724K 197M 1% /run
/dev/vda1 50G 20G 28G 42% /
tmpfs 986M 0 986M 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 986M 0 986M 0% /sys/fs/cgroup
tmpfs 198M 0 198M 0% /run/user/0
tmpfs 198M 0 198M 0% /run/user/1002
/dev/loop0 4.9M 24K 4.5M 1% /mnt/mydisk
$ sudo umount /mnt/mydisk # 卸载分区
whb@bite:/mnt$ df -h
Filesystem Size Used Avail Use% Mounted on
udev 956M 0 956M 0% /dev
tmpfs 198M 724K 197M 1% /run
/dev/vda1 50G 20G 28G 42% /
tmpfs 986M 0 986M 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 986M 0 986M 0% /sys/fs/cgroup
tmpfs 198M 0 198M 0% /run/user/0
tmpfs 198M 0 198M 0% /run/user/1002
指令补充:
- dd 主要用于复制文件,并且能在复制过程中进行数据转换
- 基本语法: dd [选项] [参数] 。 常见选项 if(指定输入文件)、of(指定输出文件)
- 上面代码中 dd if=/dev/zero of=./disk.img bs=1M count=5 这个命令是从 /dev/zero(一个特殊设备文件,能提供无穷的空字符流)读取数据,输出到 disk.img 文件,bs=1M 代表块大小为1MB,count=5表示复制五个块
- .img文件 是磁盘镜像文件
- mkfs 用于在磁盘分区或存储设备上创建文件系统,
- 基本语法: mkfs[选项][设备文件名]
- 上面代码中 mkfs.ext4 ./disk.img 会将disk.img 格式化为.ext4文件系统。
- mkfs 支持多种文件系统类型,如 ext2、ext3、ext4、vfat等(vfat常用于和Windows系统兼容的移动存储设备,如U盘等)。
- mount 主要用于挂载文件系统,挂载是使存储设备(如硬盘分区、USB设备等)中的文件系统能够被操作系统访问的操作。
- 常见语法为 mount [-t 文件系统类型] [-o 挂载选项] 设备名称 挂载点 。
- 上面代码中 mount -t ext4 ./disk.img /mnt/mydisk 这表示将ext4文件系统的disk.img 分区挂载到/mnt/mydisk 目录下
- mnt目录 是系统中专门用于挂载文件系统的目录, 在Linux等系统中,当需要临时挂载外部设备(如U盘、移动硬盘)或者其他分区的文件系统时,“mnt”目录是一个合适的挂载点。它就像是一个连接外部存储和系统的中转站。例如,将一个U盘(假设设备名为/dev/sdb1)挂载到“/mnt”目录下的“usb”子目录(即/mnt/usb)后,就可以通过访问“/mnt/usb”来读写U盘中的文件。这使得系统能够方便地整合和使用外部存储资源。
- umount 用于卸载 已经挂载的文件系统
- 基本语法 umount [选项] 挂载点或设备文件名
- 上面代码中 umount /mnt/mydisk 就是通过挂载点卸载
- df 用于查看文件系统的磁盘空间使用情况
- 基本用法 df 会显示各个已挂载文件系统的相关信息。包括文件系统名称(比如Linux中ext4分区对应的设备文件名)、总容量(以KB、MB或GB等为单位)、已用空间、可用空间和挂载点。
- 常用选项 -h :以人类可读的方式显示信息 -T :除了显示空间信息外,还会显示文件系统类型。
注意:
/dev/loop0 在Linux系统中代表第一个循环设备(loop device)。循环设备,也被称为回环设备或者loopback设备,是一种伪设备(pseudo-device),它允许将文件作为块设备(block device)来使用。这种机制使得可以将文件(比如ISO镜像文件)挂载(mount)为文件系统,就像它们是物理硬盘分区或者外部存储设备一样。
3.8.2 一个结论
- 分区写入文件系统,无法直接使用,需要和指定的目录关联,进行挂载才能使用。
- 所以,可以根据访问目标文件的“路径前缀”准确判断我在哪一个分区。
3.9 文件系统总结
下面用几张图总结下
4. 软硬链接
4.1 硬连接
我们看到,真正找到磁盘上文件的并不是文件名,而是inode。其实在linux中可以让多个文件名对应于同一个inode。
[root@localhost linux]# touch abc
[root@localhost linux]# ln abc def
[root@localhost linux]# ls -li abc def
263466 abc
263466 def
这里讲解下上面的指令
- ln指令用于创建链接,链接的对象主要是文件或目录,链接分为软链接和硬链接
- 硬连接(Hard Link) 语法为 ln [源文件] [目标文件]
- 硬链接文件和源文件的索引节点相同,它们指向存储设备上同一块物理数据区域。
- 可以把它想象成一个文件有多个名字,对硬链接文件进行读写操作,就如同对源文件操作一样,因为它们本质上是同一个文件内容。
- 只有把所有指向这个内容的文件名(包括硬链接)全部删除,文件才会真正从文件系统中被删除。
- 上述代码中 abc 和 def 的链接状态完全相同,他们被称为指向文件的硬链接。 内核记录了这个连接数,inode 263466 的硬连接数为2 。
- 我们在删除文件时干了两件事情:1.在目录中将对应的记录删除。 2. 将硬链接数-1,如果为0,则将对应的磁盘资源释放。
- 硬链接本质就是一组文件名和已经存在的文件的映射关系!
4.2 软链接
硬链接是通过inode 引用另外一个文件,软链接是通过名字引用另外一个文件,但实际上,新的文件和被引用的文件的inode不同,应用常见上可以想象成一个快捷方式。在shell中的做法
1 [root@localhost linux]# ln -s abc.s abc
2 [root@localhost linux]# ls -li
3 263563 -rw-r--r--. 2 root root 0 9⽉ 15 17:45 abc
4 261678 lrwxrwxrwx. 1 root root 3 9⽉ 15 17:53 abc.s -> abc
5 263563 -rw-r--r--. 2 root root 0 9⽉ 15 17:45 def
这里同样讲解下上面代码
- 软链接(Symbolic Link,也叫符号链接) 语法:ln -s [源文件或目录] [目标文件或目录]。
- 软链接文件有自己独立的inode,它里面存储的是源文件或目录的路径。
- 相当于Winodws中的快捷方式,当访问软链接时,系统会根据其中记录的路径去查源文件或目录。如果源文件被删除,软链接就会失效。
acm时间
下面解释一下文件的三个时间:
- Access 最后访问时间
- Modify 文件内容最后修改时间
- Change 属性最后修改时间
4.3 软硬链接对比
- 本质
- 硬链接是文件的一个或多个文件名对应同一个inode(文件索引节点),可以理解为同一个文件有多个入口。
- 软链接可以跨文件系统创建,这使得它在文件系统之间建立连接更方便。
- 对目录的适用性
- 很多文件系统(包括Linux)不允许对目录创建硬链接,这是为了避免目录树中出现循环引用等复杂问题。
- 软链接可以对目录进行创建,这有助于对目录结构进行灵活的组织和调整,如可以将一个目录的快捷方式放置在其他位置。
- 稳定性
- 硬链接只要其中一个链接存在,文件数据就不会丢失,稳定性强
- 软链接如果源文件被删除或移动,链接就会失效,相对不稳定。
- 操作便利性
- 硬链接在操作时与源文件完全相同,没有额外的路径解析等操作,相对高效。
- 软链接需要系统额外解析其指向的路径来定位源文件,并且在源文件位置变化后可能需要更新链接路径。
4.4 软硬链接的用途:
硬链接
- 文件备份与防误删
- 共享文件访问 (可以实现从不同的路径访问相同的文件内容)
注: 目录中 .和.. 实际就是硬链接
软连接
- 方便文件访问(简化文件的访问路径),目录快捷方式
- 跨文件系统链接(软链接可以用于连接不同文件系统中的文件,因为软链接实质只是文件路径)