✨✨所属专栏:Linux✨✨
✨✨作者主页:嶔某✨✨
理解硬件
磁盘
磁盘,顾名思义就是用磁性来存储信息的。磁盘是一种永久性存储介质,在计算机中,磁盘几乎是唯一的机械设备。与磁盘相对应的就是内存,但是内存是掉电易失存储介质,所以目前所有的普通文件都是在磁盘中存储的。
基本结构
- 磁盘是一个块(block)设备,扇区是从磁盘读出和写⼊信息的最⼩单位,通常⼤⼩为 512 字节。
- 磁头(head)数:每个盘⽚⼀般有上下两⾯,分别对应1个磁头,共2个磁头
- 磁道(track)数:磁道是从盘⽚外圈往内圈编号0磁道,1磁道...,靠近主轴的同⼼圆⽤于停靠磁头,不存储数据
- 柱⾯(cylinder)数:磁道构成柱⾯,数量上等同于磁道个数
- 扇区(sector)数:每个磁道都被切分成很多扇形区域,每道的扇区数量相同
- 圆盘(platter)数:就是盘⽚的数量
- 磁盘容量=磁头数 × 磁道(柱⾯)数 × 每道扇区数 × 每扇区字节数
- 细节:传动臂上的磁头是共进退的
CHS寻址
那么我们如何定位一个扇区呢?(CHS地址定位)
- 定位磁头(head)
- 确定磁头要访问哪一个柱面(磁道)(cylinder)
- 定位扇区(sector)
对于CHS寻址,系统用8bit来存储磁头地址,用10bit来存储柱面地址,用6bit来存储扇区地址,一个扇区的容量为512Byte,那么用CHS寻址一块硬盘的最大容量为(256*1024*63*512B)/(1024*1024)=8064MB(若1MB=1000000B就是8.4GB)这显然不满足当今计算机对硬盘的要求。
LBA寻址(Logical Block Addressing)
我们知道,一个磁道上有很多扇区,我们把一个磁道的扇区从1编号然后把它们展开:
然后磁盘的一个柱面有许多个盘面,也就有许多的磁道,我们把这一个柱面的所有磁道里的扇区全部展开排列,其实就是一个二维数组。
之后,在每一个盘面上不止有一圈磁道,尽管是内外圈,但是它们的扇区数量一般是一样的。我们把所有柱面的所有磁道里面的所有扇区全部展开:
这就是一个三维数组,所以CHS寻址寻找一个扇区就是找到这个三维数组的三个下标。
我们有了C/C++的基础,在我们看来,多维数组其本质就是一维数组,在这个一维数组里,每一个扇区都有且只有一个独立编号,这个编号就是LBA地址。操作系统只需要知道LBA地址,然后由磁盘转化为CHS地址
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使⽤磁盘,就可以⽤⼀个数字访问磁盘扇区了。
EXT2文件系统
“块”
其实磁盘是典型的块设备,操作系统与磁盘的IO其实不是以扇区为单位的,而是以一个“块”(block)为单位。常见的是以8个扇区为一个“块”,也就是4KB,“块”是文件存取的最小单位。
- 磁盘是一个三维数组,我们把它看作一维数组,数组的下标就是LBA地址,每个元素就是扇区。
- 每个扇区都有LBA地址,那么8个扇区一个“块”,每一个块的地址也能算出来。
- LBA = 快号 * 8 + n(块内部的第n个扇区)
“分区”
磁盘是可以被分成多个区域的,在windows中,我们把磁盘分为C、D、E盘。C、D、E各个盘就是分区。分区其实就是对磁盘的一种格式化。那么在Linux下是怎么分区的嘞?
其实柱面是分区的最小单位,我们利用参考柱面号码的方式来分区。其实就是设置每个分区的开始和结束柱面号。柱面大小一致,柱面里的扇区大小、个数一致。只需要知道起始柱面号和结束柱面号,知道一个柱面多少扇区,每个扇区多大,那么分区的大小和LBA也就清楚了。
“inode”
之前我们说,文件 = 属性 + 数据,我们使用ls -l 的时候除了看到文件名,还能看到文件属性:
ubuntu@VM-4-4-ubuntu:~$ ls -l
total 4
drwxrwxr-x 12 ubuntu ubuntu 4096 Jan 18 23:32 Code
| | | | | | |
| | | | | | |
权限 硬链接数 所有者 所属组 大小 最后修改时间 文件名
这个过程是读取磁盘上的信息然后显示出来
还有一个命令能够看到更多文件属性信息:stat
ubuntu@VM-4-4-ubuntu:~$ stat Code
File: Code
Size: 4096 Blocks: 8 IO Block: 4096 directory
Device: 253,2 Inode: 524305 Links: 12
Access: (0775/drwxrwxr-x) Uid: ( 1000/ ubuntu) Gid: ( 1000/ ubuntu)
Access: 2025-01-18 23:32:28.245182658 +0800
Modify: 2025-01-18 23:32:19.021839203 +0800
Change: 2025-01-18 23:32:19.021839203 +0800
Birth: 2024-11-08 22:16:06.255019588 +0800
那么,文件数据都储存在“块”中,那么文件的属性(创建者、创建日期、文件大小)存储在哪里呢?inode 中文译为“索引节点”,通过ls -li查看inode编号:
ubuntu@VM-4-4-ubuntu:~$ ls -il
total 4
524305 drwxrwxr-x 12 ubuntu ubuntu 4096 Jan 18 23:32 Code
|
|
inode_number
- 每个文件都有对应的inode,里面包含了与该文件有关的一些信息。
- Linux下文件的存储时属性和内容分离的。
- Linux下,保存文件属性的集合叫inode,一个文件,一个inode,inode内部由一个唯一标识符,叫inode号(inode_number)【其实这个号码只在当前分区是唯一的】
/*
* Structure of an inode on the disk
*/
struct ext2_inode {
__u16 i_mode; /* File mode */
__u16 i_uid; /* Owner Uid */
__u32 i_size; /* Size in bytes */
__u32 i_atime; /* Access time */
__u32 i_ctime; /* Creation time */
__u32 i_mtime; /* Modification time */
__u32 i_dtime; /* Deletion Time */
__u16 i_gid; /* Group Id */
__u16 i_links_count; /* Links count */
__u32 i_blocks; /* Blocks count */
__u32 i_flags; /* File flags */
union {
struct {
__u32 l_i_reserved1;
} linux1;
struct {
__u32 h_i_translator;
} hurd1;
struct {
__u32 m_i_reserved1;
} masix1;
} osd1; /* OS dependent 1 */
__u32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
__u32 i_version; /* File version (for NFS) */
__u32 i_file_acl; /* File ACL */
__u32 i_dir_acl; /* Directory ACL */
__u32 i_faddr; /* Fragment address */
union {
struct {
__u8 l_i_frag; /* Fragment number */
__u8 l_i_fsize; /* Fragment size */
__u16 i_pad1;
__u32 l_i_reserved2[2];
} linux2;
struct {
__u8 h_i_frag; /* Fragment number */
__u8 h_i_fsize; /* Fragment size */
__u16 h_i_mode_high;
__u16 h_i_uid_high;
__u16 h_i_gid_high;
__u32 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 */
};
- 文件名并未储存在inode内部。(文件名大小不定,不方便inode的统一管理)
- inode大小一般是128字节或256字节,我们统一认为是128字节。
- 任何文件的内容大小可以不同,但是属性大小一定是相同的。
宏观认识
超级块(Super block)
存放文件系统本身的结构信息,描述整个分区的文件系统信息。主要信息有:block和inode的总量,未使用的block和inode数量,一个block和inode的大小,最近一次挂载时间,最近一次写入数据时间,最近一次检验磁盘的时间等其他文件系统相关的信息。Super Block的信息被破坏了,可以说整个文件系统就被破坏了。
Super Block在每个块组的开头都有一份拷⻉(第⼀个块组必须有,后⾯的块组可以没有)。 为了保证⽂件系统在磁盘部分扇区出现物理问题的情况下还能正常⼯作,就必须保证⽂件系统的Super Block信息在这种情况下也能正常访问。所以⼀个⽂件系统的Super Block会在多个Block Group中进⾏备份,这些Super Block区域的数据保持⼀致。
/*
* Structure of the super block
*/
struct ext2_super_block {
__u32 s_inodes_count; /* Inodes count */
__u32 s_blocks_count; /* Blocks count */
__u32 s_r_blocks_count; /* Reserved blocks count */
__u32 s_free_blocks_count; /* Free blocks count */
__u32 s_free_inodes_count; /* Free inodes count */
__u32 s_first_data_block; /* First Data Block */
__u32 s_log_block_size; /* Block size */
__s32 s_log_frag_size; /* Fragment size */
__u32 s_blocks_per_group; /* # Blocks per group */
__u32 s_frags_per_group; /* # Fragments per group */
__u32 s_inodes_per_group; /* # Inodes per group */
__u32 s_mtime; /* Mount time */
__u32 s_wtime; /* Write time */
__u16 s_mnt_count; /* Mount count */
__s16 s_max_mnt_count; /* Maximal mount count */
__u16 s_magic; /* Magic signature */
__u16 s_state; /* File system state */
__u16 s_errors; /* Behaviour when detecting errors */
__u16 s_minor_rev_level; /* minor revision level */
__u32 s_lastcheck; /* time of last check */
__u32 s_checkinterval; /* max. time between checks */
__u32 s_creator_os; /* OS */
__u32 s_rev_level; /* Revision level */
__u16 s_def_resuid; /* Default uid for reserved blocks */
__u16 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...
*/
__u32 s_first_ino; /* First non-reserved inode */
__u16 s_inode_size; /* size of inode structure */
__u16 s_block_group_nr; /* block group # of this superblock */
__u32 s_feature_compat; /* compatible feature set */
__u32 s_feature_incompat; /* incompatible feature set */
__u32 s_feature_ro_compat; /* readonly-compatible feature set */
__u32 s_reserved[230]; /* Padding to the end of the block */
};
GDT(Group Descriptor Table)
块块组描述符表,描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储⼀个块组的描述信息,如在这个块组中从哪⾥开始是inode Table,从哪⾥开始是Data Blocks,空闲的inode和数据块还有多少个等等。块组描述符在每个块组的开头都有⼀份拷⻉。
/*
* Structure of a blocks group descriptor
*/
struct ext2_group_desc
{
__u32 bg_block_bitmap; /* Blocks bitmap block */
__u32 bg_inode_bitmap; /* Inodes bitmap block */
__u32 bg_inode_table; /* Inodes table block */
__u16 bg_free_blocks_count; /* Free blocks count */
__u16 bg_free_inodes_count; /* Free inodes count */
__u16 bg_used_dirs_count; /* Directories count */
__u16 bg_pad;
__u32 bg_reserved[3];
};
块位图(Block Bitmap)
- Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用
inode位图(Inode Bitmap)
- 每个bit表示一个inode是否闲置可用。
i节点表(inode Table)
- 存放文件属性,如文件大小,所有者,最近修改时间等。
- 当前分组的所有inode属性的集合。
- inode编号以分区为单位,整体划分,不可跨分区。
Data Block
- 数据区,存放文件内容,也就是一个一个的Block。根据不同的文件类型有以下几种情况:
- 对于普通文件文件的数据存储在数据块中。
- 对于目录,该目录下的所有文件名和目录名就相当于这个目录的数据存储在所在目录的数据块中,除了文件名外,ls -l命令看到的其他信息保存在该文件的inode中。
- Block号按照分区划分,不可跨分区。
inode与Data Block映射
inode内部存在 __u32 i_block[EXT2_N_BLOCKS]]; /* Pointers to blocks */
这个就是用来进行inode和Data block映射的,这样找到了文件属性就能找到文件内容。
分区之后的格式化操作,就是对分区进行分组,在每个分组中写入SB、GDT、Block、Bitmap、Inode Bitmap等管理信息,这些管理信息统称为:文件系统
知道了inode号,就能在指定分区中确定是哪⼀个分组,进⽽在哪⼀个分组确定是哪⼀个inode,之后文件的属性和内容就全都有了。
目录与文件名
目录也是文件,磁盘上根本没有目录的概念,只有文件属性+文件内容。目录的属性就是那些,内容保存的是文件名和inode号的映射关系。
用户在访问文件时不会用inode号,都是用的文件名。访问文件,先打开当前目录,根据文件名,获得inode号,进行文件访问。所以知道当前的工作目录是很重要的。
路径解析
那么要访问当前目录也需要知道inode号呀。当前目录的inode号在上级目录里,上级的在上上级里面。递归下去,就到了根目录“/”
实际上任何文件都有路径,访问一个 /home/ubuntu/Code/test.c 文件都要从根目录开始,依次打开每个目录,根据目录名,依次访问每个目录下指定的目录,直到访问到 test.c 这个过程叫做Liunx路径解析。
所以访问文件必须要路径( = 目录+文件名)的原因
用户访问文件,本质都是进程在访问,进程都有 cwd 进程提供路径。我们open文件时也提供了路径。那么最开始的路径从哪里来?根目录 根目录固定文件名、inode号,无需查找,系统开机后就知道。
我们新建目录,其实就是在磁盘文件系统中新建目录文件。我们新建的目录或者文件都是在系统指定的目录下新建的,所以路径就是这么来的。系统 + 用户共同构建Linux的路径结构。
路径缓存
Linux下根本不存在目录,只有文件属性+文件内容。
访问任何文件都要从根目录开始解析吗?这未免也太慢了吧?
Linux会缓存历史路径结构,如果打开的文件时目录,OS会在内存中进行路径维护
维护树状的路径结构的内核结构体: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结构,缓存新路径
挂载分区
我们能根据inode在指定分区找文件,能根据目录文件内容找到指定的inode,在分区内为所欲为。
但是Linux可能有多个分区,Block_Group_number和inode_number直在一个分区内唯一。我怎么知道我要找的文件在哪个分区?
分区写⼊⽂件系统,⽆法直接使⽤,需要和指定的⽬录关联,进⾏挂载才能使用。所以,可以根据访问⽬标⽂件的"路径前缀"准确判断我在哪⼀个分区。
软硬链接
硬链接
我们看到,真正找到磁盘上⽂件的并不是⽂件名,⽽是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
- abc和def的链接状态完全相同,他们被称为指向⽂件的硬链接。内核记录了这个连接数,inode 263466 的硬连接数为2。
- 我们在删除⽂件时⼲了两件事情:1.在⽬录中将对应的记录删除,2.将硬连接数-1,如果为0,则 将对应的磁盘释放。
- . 和 .. 就是硬链接,硬链接可以做文件备份。
软连接
[root@localhost linux]# ln -s abc.s abc
[root@localhost linux]# ls -li
263563 -rw-r--r--. 2 root root 0 9⽉ 15 17:45 abc
261678 lrwxrwxrwx. 1 root root 3 9⽉ 15 17:53 abc.s -> abc
263563 -rw-r--r--. 2 root root 0 9⽉ 15 17:45 def
文件时间(ACM)
Access
: 文件最后被访问的时间。Modify
: 文件内容最后的修改时间。Change
: 文件属性最后的修改时间。
Linux下一切皆文件
上图中的外设,每个设备都可以有⾃⼰的read、write,但⼀定是对应着不同的操作⽅法!!但通过 struct file 下 file_operation 中的各种函数回调,让我们开发者只⽤file便可调取 Linux 系统中绝⼤部分的资源!!这便是“linux下⼀切皆⽂件”的核⼼理解。
这种实现方式与C++中的多态类似。在 C++中,父类指针指向谁,调用的就是谁的方法。在 C 语言中,可以通过函数指针做到指向不同的对象时执行不同的方法,实现多态的性质。在Linux中,每个struct file中包含很多函数指针,这样在struct file上层看来,所有的文件都是调用统一的接口,而在底层则通过函数指针指向不同硬件的方法,实现与具体硬件对应的逻辑。
本期博客到这里就结束了,如果有什么错误,欢迎指出,如果对你有帮助,请点个赞,谢谢!