资讯动态

OS37.【Linux】简单理解EXT2文件系统(2)

发布时间:2026/8/26 15:05:23 来源:尧图企业网站定制
目录1.知识回顾2.EXT2文件系统推算出EXT2文件系统的i_block最多能存储的数据块的个数每个数据块的大小是可以手动设置的inode bitmapblock bitmap删除一个文件需不需要将存储文件内容的数据块都清空呢?分区满的含义Group Descriptors★Super Block超级块的分类主超级块备份超级块结论3.问题: 文件的增删改查,系统做了什么?新建文件删除文件结论查找文件使用stat命令查看文件的元数据修改文件4.如何知道一个文件的inode编号5.如何理解目录?目录的数据块的内容证明目录内容是文件和inode的映射关系回顾目录的rwx权限目录的inode编号怎么找?用相对路径访问文件时底层的处理方法EXT2文件系统解决文件访问过慢的问题6.如何理解有些基础的系统调用不仅复杂,还需要基础设施的支持,举一个例子?7.参考资料1.知识回顾参见OS35.【Linux】简单理解EXT2文件系统(1)文章回顾2.EXT2文件系统上篇文章讲了i_block数组的四个级别的索引推算出EXT2文件系统的i_block最多能存储的数据块的个数1. i_block[0]~i_block[11]→12个数据块2. i_block[12],假设其指向存储数据块号的表能存N个元素→N*1N个数据块3. i_block[13],假设指针表能存储M个指针(二级间接索引),每个指针又指向能存储P个元素的存储数据块号的表→M*P个数据块4. i_block[13],假设存储二级指针的表能存储Q个指针,每个指针又指向能存储R个元素的存储一级间接索引的表,每个一级间接索引的表能存储S个元素所以最多能存储12NM*PQ*R*S个数据块,假设每个数据块的大小为K,得出EXT2允许一个文件的最大的大小为(12NM*PQ*R*S)*K每个数据块的大小是可以手动设置的具体每个数据块的大小可以使用-b选项设置,但注意: 每个数据块的大小必须是2的次方数,例如1024字节、2048字节、4096字节......8号手册中mke2fs中-b选项的说明: 手册中明确指出每个数据块的大小必须是2的次方数inode bitmapinode bitmap记录哪些inode已经使用,哪些inode没有使用,即将比特位的位置和inode编号映射起来,那么可以快速查询哪些inode编号没有用过,从而分配给文件使用block bitmap从inode bitmap同理可得block bitmap的作用,这里再赘述删除一个文件需不需要将存储文件内容的数据块都清空呢?答: 没有必要,为了提高EXT2文件系统的工作效率,只需要修改inode bitmap和block bitmap,将该文件对应的位置都清空即可除此之外,如果用户误删文件,可以使用恢复软件,恢复软件可以通过读Linux日志来得出被删除文件的inode编号,之后恢复inode bitmap,接着在inode table找到inode结构体,再找到blocks数组,然后修改block bitmap,由于被删除文件的数据块没有清空,那么恢复软件可以通过读文件块来恢复用户误删的文件分区满的含义分别设想inode编号和数据块用完的情况一个分区所用的inode的总数是固定的,这个在格式化时就已经确定1.如果inode编号用完了,但数据块还有,那么剩下的这些数据块是能存储文件内容的,虽然不能新建文件,但是可以对现有文件增加文件内容2.如果数据块用完了,而inode编号还有,那么剩下的这些inode编号是不能分配给新建文件的3.如果inode编号和数据块都恰好用完,那么显然分区是不能存储文件的在https://unix.stackexchange.com/questions/26598/how-can-i-increase-the-number-of-inodes-in-an-ext4-filesystem中被采纳回答的评论也提到了:Group Descriptors组描述符Group Descriptor,缩写是GDT(注意全局描述符表Global Descriptior Table的缩写也是GDT),其记录每个块组的信息每个块组(block group)都有组描述符,存储inode表的指针、数据块的指针、inode和数据块分配位图的指针★Super Block超级块Super Block: 存储文件系统的基本信息,包含的是整个分区的使用情况,例如一共有多少个组、每个组的大小、每个组inode的数量、每个组的block数量、每个组的起始inode、文件系统的类型与名称等等,注: 超级块的所有信息将在下一篇的实验中讲不是每个块组都有超级块,这个也在下一篇的实验中讲由此得知: 超级块很重要,如果超级块损坏了,会影响所有组的边界,进而导致磁盘的所有分区无法识别,因此必须备份超级块,这样在某些情况下可以修复文件系统超级块的分类主超级块即0号块组的超级块,紧邻着启动块备份超级块分布在文件系统的各个块组(Block group)中,但不是每个块组都有备份的超级块,这个会在下一篇文章的实验中说明结论每一个分区在被使用之前,都必须提前先将部分文件系统的属性信息提前设置进对应的分区中,方便后续使用这个分区或者分组,这叫先描述再组织3.问题: 文件的增删改查,系统做了什么?新建文件新建文件在表面上是在某一个路径下新建但其内部操作比较复杂: 一个存储设备可能分成了很多分区,而且文件不能跨分区存储,因为每个分区可以有自己的文件系统,而文件系统中的inode和文件数据块是存储在同一个文件系统内的,也就存储在同一个分区中,因此不能跨文件系统存储1.在将文件存储到设备中需要指定设备上的某一个分区2.需要为新的文件分配inode,但一个文件系统的inode编号是有限的,因此需要先确保inode编号是否充足确保inode编号是否充足需要查组描述符,Linux内核的/fs/ext2.h中是这样写的:/* * Structure of a blocks group descriptor */ 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]; };其中bg_free_inodes_count表示空闲的inode编号的个数如果组描述符中inode还多着,那么就分配指定分区中的inode3.查inode bitmap,找最近没有使用的inode编号ext2_group_desc结构体中有bg_inode_bitmap4.在inode table中找到对应的inode结构体,填写文件属性5.分析block bitmap,找对应的data block,写入文件内容删除文件本文上方说明过:删除一个文件不需要将存储文件内容的数据块都清空,即不需要清空存储文件内容的数据块1.找到删除文件对应的inode 分析文件属性2.根据所处目录找到文件存储的分区3.根据inode所处的范围确定是哪个分组的,再索引block bitmap清空文件对应的块号,即从1置为03.再索引inode bitmap,将对应的比特位从1置为0结论因为删除文件即将这个文件和对应的数据块解除绑定,可能在新建其他文件或修改其他文件时会用到删除文件对应的数据块,那么文件系统认为该删除的文件的数据块的空间被腾出来了删除,即允许被覆盖查找文件查找一个目录中的某个文件的前提是: 这个目录是可读的,如果找到某个文件和其inode的映射关系,就可以读取文件内容例如cat test.txt: 拿到inode找到对应的分区,根据inode确定分组,拿inode到inode bitmap确认是0还是1,是1再拿inode table对应的结构体数据,找到对应的data block使用stat命令查看文件的元数据例如:stat test.txt可以查到该文件的inode编号修改文件和上面类似,不再赘述4.如何知道一个文件的inode编号删除、查找、修改文件需要指定文件的inode编号,但使用者从来没关心过inode,用的是文件名5.如何理解目录?由于Linux的一切皆文件的设计思想,那么目录也是文件,文件内容属性,因此目录有自己的inode,有自己的属性,有自己的数据块......目录的数据块的内容目录的数据块存放该目录下文件的文件名和对应文件的inode的映射关系,即key-value结构由于key-value结构的特殊性,之前在C的map容器中讲过,key一定是唯一的,那么有结论: 同一个目录下不能有同名文件Linux内核的/fs/ext2/ext2.h中给出了存储文件名的结构体:/* * Structure of a directory entry */ struct ext2_dir_entry { __le32 inode; /* Inode number */ __le16 rec_len; /* Directory entry length */ __le16 name_len; /* Name length */ char name[]; /* File name, up to EXT2_NAME_LEN */ }; /* * The new version of the directory entry. Since EXT2 structures are * stored in intel byte order, and the name_len field could never be * bigger than 255 chars, its safe to reclaim the extra byte for the * file_type field. */ struct ext2_dir_entry_2 { __le32 inode; /* Inode number */ __le16 rec_len; /* Directory entry length */ __u8 name_len; /* Name length */ __u8 file_type; char name[]; /* File name, up to EXT2_NAME_LEN */ };结论: 一个目录包含很多目录项结构体(旧版是ext2_dir_entry,新版是ext2_dir_entry_2),而目录项结构体存储着文件名(char name[])和文件对应的inode(__le32 inode)证明目录内容是文件和inode的映射关系可以使用opendir函数和readdir系统调用做:man手册中给出了opendir的声明:DIR *opendir(const char *name);其返回一个DIR结构体的指针glibc 2.42中DIR结构体的定义:/dirent/dirent.h中/* This is the data type of directory stream objects. The actual structure is opaque to users. */ typedef struct __dirstream DIR;从注释中得出: DIR实例化后,是目录流对象而__dirstream被定义在/sysdeps/unix/sysv/linux/dirstream.h:/* Directory stream type. The miscellaneous Unix readdir implementations read directory data into a buffer and return struct dirent * pointers into it. */ struct __dirstream { int fd; /* File descriptor. */ __libc_lock_define (, lock) /* Mutex lock for this structure. */ size_t allocation; /* Space allocated for the block. */ size_t size; /* Total valid data in the block. */ size_t offset; /* Current offset into the block. */ off_t filepos; /* Position of next entry to read. */ int errcode; /* Delayed error code. */ /* Directory block. We must make sure that this block starts at an address that is aligned adequately enough to store dirent entries. Using the alignment of void * is not sufficient because dirents on 32-bit platforms can require 64-bit alignment. We use long double here to be consistent with what malloc uses. */ char data[0] __attribute__ ((aligned (__alignof__ (long double)))); };注意__dirstream中一个非常重要的字段: 文件描述符,之后这个会用于readdir读取目录本身的内容int fd; /* File descriptor. */之前在OS31.【Linux】文件IO (2) 文件描述符文章讲过文件描述符:接着readdir接收DIR结构体指针,返回dirent结构体的指针,dirent定义在手册中是这样说的:The readdir() function returns a pointer to a dirent structure representing the next directory entry in the directory stream pointed to by dirp. It returns NULL on reaching the end of the directory stream or if an error occurred. In the glibc implementation, the dirent structure is defined as follows: struct dirent { ino_t d_ino; /* Inode number */ off_t d_off; /* Not an offset; see below */ unsigned short d_reclen; /* Length of this record */ unsigned char d_type; /* Type of file; not supported by all filesystem types */ char d_name[256]; /* Null-terminated filename */ };里面有d_ino字段,存储目录里面的文件的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) printf(Filename: %s, Inode: %lu\n, entry-d_name, (unsigned long)entry-d_ino); closedir(dir); return 0; }运行结果:回顾目录的rwx权限之前在OS9.【Linux】基本权限(下)文章提到过目录的rwx权限1. 如果目录没有可执行权限, 则无法cd到目录中→不让更新当前所处目录cwd软链接,无进入的权限(注: 切换目录并没有访问目录中的文件)进入一个目录的本质: 进程修改自己的cwd软链接2. 如果目录没有可读权限, 则无法用ls等命令查看目录中的文件内容→无法找到目标文件的inode3. 可写权限w: 如果目录没有可写权限, 则无法在目录中创建、更改或删除文件→就算能创建文件名和inode的映射关系,但不能写到目录文件一个文件是否能被删除,不由文件本身决定,而由其所处的目录决定目录的inode编号怎么找?上面提到了目录也是文件,那么目录有自己的inode编号,只有先获取到了目录自己的inode编号才能访问目录中的文件使用递归,大目录里面有小目录,因此找目录的inode需找上一级的目录的inode那么向上一直找,最终会找到根目录/,根目录inode编号是确定的我服务器上根目录的inode编号:我Linux mint电脑上的根目录的inode编号:我ext2文件系统、单分区U盘上的根目录的inode编号:结论: EXT类文件系统的根目录的inode编号是固定的,为2用相对路径访问文件时底层的处理方法使用相对路径可以简化命令,例如cat ./myfile/test.txt当前目录中的文件可以不写绝对路径,但系统底层是需要完整的路径的,即绝对路径,上面讲过了EXT2文件系统解决文件访问过慢的问题系统底层访问文件是从根目录开始的,设想一下如果文件路径过长,那么会导致文件访问速度过慢的问题EXT2文件系统解决方法:使用缓存在Louis-Dominique Dubeau所著的Analysis of the Ext2fs structure文章中有提到Linux ext2fs manager caches access to the inodes and blocks bitmaps. This cache is a list of buffers ordered from the most recently used to the last recently used buffer. Managers should use the same kind of bitmap caching or other similar method of improving access time to disk.可以看到使用了LRU(theLastRecentlyUsed)缓存算法可以看看https://baike.baidu.com/item/dentry/6715475对目录项缓存的描述目录项缓存dcache通过dentry_hashtable哈希表和dentry_unused链表维护属于slab缓存机制保存在全局变量dentry_cache中 [2] [4]。该缓存减少了文件路径遍历层级加速文件查找过程 [5-6]。dentry_operations结构定义d_revalidate、d_hash等操作方法其父子关系通过d_parent和d_child指针实现层级遍历 [1-2]。6.如何理解有些基础的系统调用不仅复杂,还需要基础设施的支持,举一个例子?这里为了保证权威性,我直接引用Linux之父Linus Torvalds在Just For Fun的英文版第81页的叙述:My original goal was to create an operating system that Icould eventually use as a replacement for Minix. It didnt have to domore than Minix, but it had to do the things in Minix that I caredabout, and some other things I cared about, too. For example, notonly was the Minix terminal emulation bad , but there was no way ofperforming the job-control function-putting a program in thebackground while youre not using it. And memory managementwas done very simplistically, as it still is in the Mac OS, incidentally.The way you create an operating system is to find out whatthe system calls are supposed to do, and then write your own programto implement those system calls in your own way. Generallyspeaking, there are a couple of hundred system calls . Some of themcan represent multiple functions . Others are quite simple. Some ofthe more fundamental system calls are really complicated anddepend on a great deal ofinfrastructure(注: 中译版将这个词翻译为设备是不对的,Linus说的是许多抽象层!! 看后文就知道了)being there.Take the systemcalls of Write and Read . You need to create a disk driver inorder to write something to disk or read something from disk. TakeOpen . You have to create the entire file system layer that parsesthe names and figures out where on the disk everything is. It tookmonths just to write the Open system call. But once it was inplace, the same code could be used for other functions .Thats how the early development was done. I was readingthe standards from either the Sun OS manual or various books, justpicking off system calls one by one and trying to make somethingthat worked . It was really frustrating.Linus举了读写磁盘文件的例子,读写磁盘文件不是一个容易的事情,涉及到软硬件交互You need to create a disk driver in order to write something to disk or read something from disk. → 直接和磁盘打交道的是磁盘驱动,鉴于linus当时的开发背景(在书本第二章BIRTH OF AN OP ERATING SYSTEM的位置),当时是linux初代版本运行在Linus自己的80386电脑上,有一块ATA盘,驱动读写磁盘方式是通过in、out汇编指令进行的,具体可以看linux 0.01的port_write宏:bootlin | linux 0.01 | /kernel/hd.c#define port_write(port,buf,nr) \ __asm__(cld;rep;outsw::d (port),S (buf),c (nr):cx,si)可以看到写的是gcc的嵌入式汇编,这里不管格式,注意到outsw,在intel SDM手册里面是这样写的:一句话,outsw将内存中的数据送到I/O端口,硬件会自己将得到的数据送入存储设备中,比如386电脑的ATA硬盘扇区中结论: 在x86中,CPU和各个外设是可以(其实还有别的方法,这里不讲)通过端口进行通信You have to create the entire file system layer that parses the names and figures out where on the disk everything is → 将数据写入磁盘,中间需要经过文件系统层,文件系统规定了磁盘上文件的布局方式,文件系统的知识本文讲过了,这里不在赘述所以针对早期的linux,当时Linus描述的许多抽象层是用户层 → 文件系统层 → 驱动层 → 硬件层,后期linus加入了VFS虚拟文件系统层,这里不讲7.参考资料1.NMU大学的课程笔记(部分内容有错误,后面的文章会说)https://euclid.nmu.edu/~rappleto/Classes/CS426/Notes/FileSystems/EXT2/2.EXT2文件系统作者的论文 Design and Implementation of the Second Extended Filesystem3. 张书宁老师的《文件系统技术内幕大数据时代海量数据存储之道》4.高剑林老师的《Linux内核探秘 深入解析文件系统和设备驱动的架构与设计》5.Linux内核文档https://elixir.bootlin.com/linux/v6.19-rc1/source/Documentation/filesystems/ext2.rst

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价