全国大型免费网站建设知乎网站建设

当前位置: 首页 > news >正文

全国大型免费网站建设,知乎网站建设,手机网站设计制作服务,萧云建设网站一#xff0c;文件系统特点 文件系统要有严格的组织形式#xff0c;使得文件能够以块为单位进行存储。文件系统中也要有索引区#xff0c;用来方便查找一个文件分成的多个块都存放在了什么位置。如果文件系统中有的文件是热点文件#xff0c;近期经常被读取和写入#xf…一文件系统特点 文件系统要有严格的组织形式使得文件能够以块为单位进行存储。文件系统中也要有索引区用来方便查找一个文件分成的多个块都存放在了什么位置。如果文件系统中有的文件是热点文件近期经常被读取和写入文件系统应该有缓存层。文件应该用文件夹的形式组织起来方便管理和查询。Linux内核要在自己的内存里面维护一套数据结构来保存哪些文件被哪些进程打开和使用。 总体来说文件系统的主要功能梳理如下 二EXT系列的文件系统的格式 2.1,inode与块的存储 硬盘分成相同大小的单元我们称为块Block。一块的大小是扇区大小的整数倍默认是4K。在格式化的时候这个值是可以设定的。 一大块硬盘被分成了一个个小的块用来存放文件的数据部分。这样一来如果我们像存放一个文件就不用给他分配一块连续的空间了。我们可以分散成一个个小块进行存放。这样就灵活得多也比较容易添加、删除和插入数据。 inode就是文件索引的意思我们每个文件都会对应一个inode一个文件夹就是一个文件也对应一个inode。 2.2,Exents是一个树状结构 每个节点都有一个头ext4_extent_header可以用来描述某个节点 struct ext4_extent_header {le16 eh_magic; /* probably will support different formats */le16 eh_entries; /* number of valid entries /__le16 eh_max; / capacity of store in entries /__le16 eh_depth; / has tree real underlying blocks? /__le32 eh_generation; / generation of the tree / };eh_entries表示这个节点里面有多少项。这里的项分两种如果是叶子节点这一项会直接指向硬盘上的连续块的地址我们称为数据节点ext4_extent如果是分支节点这一项会指向下一层的分支节点或者叶子节点我们称为索引节点ext4_extent_idx。这两种类型的项的大小都是12个byte。 /** This is the extent on-disk structure. Its used at the bottom of the tree./ struct ext4_extent {__le32 ee_block; / first logical block extent covers /__le16 ee_len; / number of blocks covered by extent /__le16 ee_start_hi; / high 16 bits of physical block /__le32 ee_start_lo; / low 32 bits of physical block / }; /** This is index on-disk structure. Its used at all the levels except the bottom./ struct ext4_extent_idx {__le32 ei_block; / index covers logical blocks from block /__le32 ei_leaf_lo; / pointer to the physical block of the next ** level. leaf or next index could be there /__le16 ei_leaf_hi; / high 16 bits of physical block */u16 ei_unused; };如果文件不大inode里面的i_block中可以放得下一个ext4_extent_header和4项ext4_extent。所以这个时候eh_depth为0也即inode里面的就是叶子节点树高度为0。 如果文件比较大4个extent放不下就要分裂成为一棵树eh_depth0的节点就是索引节点其中根节点深度最大在inode中。最底层eh_depth0的是叶子节点。 除了根节点其他的节点都保存在一个块4k里面4k扣除ext4_extent_header的12个byte剩下的能够放340项每个extent最大能表示128MB的数据340个extent会使你的表示的文件达到42.5GB。 2.3,inode位图和块位图 inode的位图大小为4k每一位对应一个inode。如果是1表示这个inode已经被用了如果是0则表示没被用。block的位图同理。 在Linux操作系统里面想要创建一个新文件会调用open函数并且参数会有O_CREAT。这表示当文件找不到的时候我们就需要创建一个。那么open函数的调用过程大致是要打开一个文件先要根据路径找到文件夹。如果发现文件夹下面没有这个文件同时又设置了O_CREAT就说明我们要在这个文件夹下面创建一个文件。 创建一个文件那么就需要创建一个inode那么就会从文件系统里面读取inode位图然后找到下一个为0的inode就是空闲的inode。对于block位图在写入文件的时候也会有这个过程。 2.4,文件系统的格式 数据块的位图是放在一个块里面的共4k。每位表示一个数据块共可以表示 个数据块。如果每个数据块也是按默认的4K最大可以表示空间为 个byte也就是128M那么显然是不够的。这个时候就需要用到块组数据结构为ext4_group_desc这里面对于一个块组里的inode位图bg_inode_bitmap_lo、块位图bg_block_bitmap_lo、inode列表bg_inode_table_lo都有相应的成员变量。 这样一个个块组就基本构成了我们整个文件系统的结构。因为块组有多个块组描述符也同样组成一个列表我们把这些称为块组描述符表。 资料直通车最新Linux内核源码资料文档视频资料https://docs.qq.com/doc/DTmFTc29xUGdNSnZ2 内核学习地址Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈https://ke.qq.com/course/4032547?flowToken1040236 我们还需要有一个数据结构对整个文件系统的情况进行描述这个就是超级块ext4_super_block。里面有整个文件系统一共有多少inodes_inodes_count一共有多少块s_blocks_count_lo每个块组有多少inodes_inodes_per_group每个块组有多少块s_blocks_per_group等。这些都是这类的全局信息。 最终整个文件系统格式就是下面这个样子 默认情况下超级块和块组描述符表都有副本保存在每一个块组里面。防止这些数据丢失了导致整个文件系统都打不开了。 由于如果每个块组里面都保存一份完整的块组描述符表一方面很浪费空间另一个方面由于一个块组最大128M而块组描述符表里面有多少项这就限制了有多少个块组128M * 块组的总数目是整个文件系统的大小就被限制住了。 因此引入Meta Block Groups特性。 首先块组描述符表不会保存所有块组的描述符了而是将块组分成多个组我们称为元块组Meta Block Group。每个元块组里面的块组描述符表仅仅包括自己的一个元块组包含64个块组这样一个元块组中的块组描述符表最多64项。 我们假设一共有256个块组原来是一个整的块组描述符表里面有256项要备份就全备份现在分成4个元块组每个元块组里面的块组描述符表就只有64项了这就小多了而且四个元块组自己备份自己的。 根据图中每一个元块组包含64个块组块组描述符表也是64项备份三份在元块组的第一个第二个和最后一个块组的开始处。 如果开启了sparse_super特性超级块和块组描述符表的副本只会保存在块组索引为0、3、5、7的整数幂里。所以上图的超级块只在索引为0、3、5、7等的整数幂里。 三目录的存储格式 其实目录本身也是个文件也有inode。inode里面也是指向一些块。和普通文件不同的是普通文件的块里面保存的是文件数据而目录文件的块里面保存的是目录里面一项一项的文件信息。这些信息我们称为ext4_dir_entry。 在目录文件的块中最简单的保存格式是列表每一项都会保存这个目录的下一级的文件的文件名和对应的inode通过这个inode就能找到真正的文件。第一项是“.”表示当前目录第二项是“…”表示上一级目录接下来就是一项一项的文件名和inode。 如果在inode中设置EXT4_INDEX_FL标志那么就表示根据索引查找文件。索引项会维护一个文件名的哈希值和数据块的一个映射关系。 如果我们要查找一个目录下面的文件名可以通过名称取哈希。如果哈希能够匹配上就说明这个文件的信息在相应的块里面。然后打开这个块如果里面不再是索引而是索引树的叶子节点的话那里面还是ext4_dir_entry的列表我们只要一项一项找文件名就行。通过索引树我们可以将一个目录下面的N多的文件分散到很多的块里面可以很快地进行查找。 四Linux中的文件缓存 4.1ext4文件系统层 对于ext4文件系统来讲内核定义了一个ext4_file_operations const struct file_operations ext4_file_operations { …….read_iter ext4_file_read_iter,.write_iter ext4_file_write_iter, …… }ext4_file_read_iter会调用generic_file_read_iterext4_file_write_iter会调用generic_file_write_iter ssize_t generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter) { ……if (iocb-ki_flags IOCB_DIRECT) { ……struct address_space *mapping file-f_mapping; ……retval mapping-a_ops-direct_IO(iocb, iter);} ……retval generic_file_buffered_read(iocb, iter, retval); }ssize_t generic_file_write_iter(struct kiocb *iocb, struct iov_iter *from) { ……if (iocb-ki_flags IOCB_DIRECT) { ……written generic_file_direct_write(iocb, from); ……} else { ……written generic_perform_write(file, from, iocb-ki_pos); ……} }generic_file_read_iter和generic_file_write_iter有相似的逻辑就是要区分是否用缓存。因此根据是否使用内存做缓存我们可以把文件的I/O操作分为两种类型。 第一种类型是缓存I/O。大多数文件系统的默认I/O操作都是缓存I/O。对于读操作来讲操作系统会先检查内核的缓冲区有没有需要的数据。如果已经缓存了那就直接从缓存中返回否则从磁盘中读取然后缓存在操作系统的缓存中。对于写操作来讲操作系统会先将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说写操作就已经完成。至于什么时候再写到磁盘中由操作系统决定除非显式地调用了sync同步命令。 第二种类型是直接IO就是应用程序直接访问磁盘数据而不经过内核缓冲区从而减少了在内核缓存和用户程序之间数据复制。 如果在写的逻辑generic_file_write_iter里面发现设置了IOCB_DIRECT则调用generic_file_direct_write里面同样会调用address_space的direct_IO的函数将数据直接写入硬盘。 带缓存的写入操作 我们先来看带缓存写入的函数generic_perform_write。 ssize_t generic_perform_write(struct file *file,struct iov_iter *i, loff_t pos) {struct address_space *mapping file-f_mapping;const struct address_space_operations *a_ops mapping-a_ops;do {struct page page;unsigned long offset; / Offset into pagecache page /unsigned long bytes; / Bytes to write to page */status a_ops-write_begin(file, mapping, pos, bytes, flags,page, fsdata);copied iov_iter_copy_from_user_atomic(page, i, offset, bytes);flush_dcache_page(page);status a_ops-write_end(file, mapping, pos, bytes, copied,page, fsdata);pos copied;written copied;balance_dirty_pages_ratelimited(mapping);} while (iov_iter_count(i)); }循环中主要做了这几件事 对于每一页先调用address_space的write_begin做一些准备调用iov_iter_copy_from_user_atomic将写入的内容从用户态拷贝到内核态的页中调用address_space的write_end完成写操作调用balance_dirty_pages_ratelimited看脏页是否太多需要写回硬盘。所谓脏页就是写入到缓存但是还没有写入到硬盘的页面。 对于第一步调用的是ext4_write_begin来说主要做两件事 第一做日志相关的工作 ext4是一种日志文件系统是为了防止突然断电的时候的数据丢失引入了日志Journal模式。日志文件系统比非日志文件系统多了一个Journal区域。文件在ext4中分两部分存储一部分是文件的元数据另一部分是数据。元数据和数据的操作日志Journal也是分开管理的。你可以在挂载ext4的时候选择Journal模式。这种模式在将数据写入文件系统前必须等待元数据和数据的日志已经落盘才能发挥作用。这样性能比较差但是最安全。 另一种模式是order模式。这个模式不记录数据的日志只记录元数据的日志但是在写元数据的日志前必须先确保数据已经落盘。这个折中是默认模式。 还有一种模式是writeback不记录数据的日志仅记录元数据的日志并且不保证数据比元数据先落盘。这个性能最好但是最不安全。 第二调用 grab_cache_page_write_begin来得到应该写入的缓存页。 struct page *grab_cache_page_write_begin(struct address_space *mapping,pgoff_t index, unsigned flags) {struct page *page;int fgp_flags FGP_LOCK|FGP_WRITE|FGP_CREAT;page pagecache_get_page(mapping, index, fgp_flags,mapping_gfp_mask(mapping));if (page)wait_for_stable_page(page);return page; }在内核中缓存以页为单位放在内存里面每一个打开的文件都有一个struct file结构每个struct file结构都有一个struct address_space用于关联文件和内存就是在这个结构里面有一棵树用于保存所有与这个文件相关的的缓存页。 对于第二步调用 iov_iter_copy_from_user_atomic。先将分配好的页面调用kmap_atomic映射到内核里面的一个虚拟地址然后将用户态的数据拷贝到内核态的页面的虚拟地址中调用kunmap_atomic把内核里面的映射删除。 size_t iov_iter_copy_from_user_atomic(struct page *page,struct iov_iter *i, unsigned long offset, size_t bytes) {char *kaddr kmap_atomic(page), *p kaddr offset;iterate_all_kinds(i, bytes, v,copyin((p v.iov_len) - v.iov_len, v.iov_base, v.iov_len),memcpy_from_page((p v.bv_len) - v.bv_len, v.bv_page,v.bv_offset, v.bv_len),memcpy((p v.iov_len) - v.iov_len, v.iov_base, v.iov_len))kunmap_atomic(kaddr);return bytes; }第三步中调用ext4_write_end完成写入。这里面会调用ext4_journal_stop完成日志的写入会调用block_write_end-block_commit_write-mark_buffer_dirty将修改过的缓存标记为脏页。可以看出其实所谓的完成写入并没有真正写入硬盘仅仅是写入缓存后标记为脏页。 第四步调用 balance_dirty_pages_ratelimited是回写脏页 /*** balance_dirty_pages_ratelimited - balance dirty memory state* mapping: address_space which was dirtied** Processes which are dirtying memory should call in here once for each page* which was newly dirtied. The function will periodically check the systems* dirty state and will initiate writeback if needed.*/ void balance_dirty_pages_ratelimited(struct address_space *mapping) {struct inode *inode mapping-host;struct backing_dev_info *bdi inode_to_bdi(inode);struct bdi_writeback *wb NULL;int ratelimit; ……if (unlikely(current-nr_dirtied ratelimit))balance_dirty_pages(mapping, wb, current-nr_dirtied); …… }在balance_dirty_pages_ratelimited里面发现脏页的数目超过了规定的数目就调用balance_dirty_pages-wb_start_background_writeback启动一个背后线程开始回写。 另外还有几种场景也会触发回写 用户主动调用sync将缓存刷到硬盘上去最终会调用wakeup_flusher_threads同步脏页当内存十分紧张以至于无法分配页面的时候会调用free_more_memory最终会调用wakeup_flusher_threads释放脏页脏页已经更新了较长时间时间上超过了设定时间需要及时回写保持内存和磁盘上数据一致性。 4.2带缓存的读操作 看带缓存的读对应的是函数generic_file_buffered_read。 static ssize_t generic_file_buffered_read(struct kiocb *iocb,struct iov_iter *iter, ssize_t written) {struct file *filp iocb-ki_filp;struct address_space *mapping filp-f_mapping;struct inode *inode mapping-host;for (;;) {struct page page;pgoff_t end_index;loff_t isize;page find_get_page(mapping, index);if (!page) {if (iocb-ki_flags IOCB_NOWAIT)goto would_block;page_cache_sync_readahead(mapping,ra, filp,index, last_index - index);page find_get_page(mapping, index);if (unlikely(page NULL))goto no_cached_page;}if (PageReadahead(page)) {page_cache_async_readahead(mapping,ra, filp, page,index, last_index - index);}/** Ok, we have the page, and its up-to-date, so now we can copy it to user space…*/ret copy_page_to_iter(page, offset, nr, iter);} }在generic_file_buffered_read函数中我们需要先找到page cache里面是否有缓存页。如果没有找到不但读取这一页还要进行预读这需要在page_cache_sync_readahead函数中实现。预读完了以后再试一把查找缓存页。 如果第一次找缓存页就找到了我们还是要判断是不是应该继续预读如果需要就调用 page_cache_async_readahead发起一个异步预读。 最后copy_page_to_iter会将内容从内核缓存页拷贝到用户内存空间。