The demand of persistent storage is with growth of computer. Currently we can only store things on our map memory, but it’s only exist in running time, which is Internal Storage, now we want to store it to External Storage.
Play the role of mapping the given dir tree structure to persistent storage. For example: windows-FAT/NTPS; linux-Ext3/Ext4/Btrfs. Therefore, construct a VFS-Virtual File System is necessary to restrict unified interface.
Design
Simplification
flatten: only root dir /
permission: only user and no restriction on file access
no timestamp
no soft/hard link
many…
Cache
For a persistent external storage, it will separate file in basic storage unit. Which is called sector(usually 512 bytes, 4KB), rather, file system will set its own storage unit which is called block, usually different from sector, but in this implementation, we set it as 512 bytes, same as sector.
File will not read and write directly often which will slow down speed, we will construct Block Cache to store read data. Then we unify all block cache in to a manager with limit size and used for allocation and deallocation.
Bit Map is a nice structure to handle mapping operations, we set each block as 512 bytes(4KB), each bits represent a state of allocation(1/0 for allocated/deallocated).
Based on such structure, we could exposit what is Inode and Data Block, not all block will store real data because some of them need to be used as guidance. However, we also need to know where and how these route blocks be allocated. That’s the reason of Bit Map! Now we delve into Inode.
To make one inode control many data blocks, we will design layer of route for it. Beside direct index, it also store the index of layer 1 and layer 2 to route other index block(which is considered same as data block), and route to real data block. Notice one block contains 512 bytes, which is 512 u8, so it contains 512/4 = 128 u32, so one index block can route 128 * 512 bytes = 128 * 0.5 KB = 64 KB in one layer. In second layer, it can route as much as 128 * 64 KB = 64 MB.
Such Inode take 128 bytes, so in one block, it could contains 4 inodes. We should make a data structure could be fit exactly into a block size. Now we design route method.
// impl DiskInode // pub fn read_at( // &self, // offset: usize, // buf: &mut [u8], // block_device: &Arc<dyn BlockDevice>, loop { // calculate end of current block letmut end_current_block = (start / BLOCK_SZ + 1) * BLOCK_SZ; end_current_block = end_current_block.min(end); // read and update read size let block_read_size = end_current_block - start; let dst = &mut buf[read_size..read_size + block_read_size]; get_block_cache( self.get_block_id(start_block asu32, block_device) asusize, Arc::clone(block_device), ) .lock() .read(0, |data_block: &DataBlock| { let src = &data_block[start % BLOCK_SZ..start % BLOCK_SZ + block_read_size]; dst.copy_from_slice(src); }); read_size += block_read_size; // move to next block if end_current_block == end { break; } start_block += 1; start = end_current_block; }
File System
Due to our consecutive layout, we will store bitmap and start block, then initiate a unified system to control allocation and route. We call it File System.
Our Disk Inode is aims for underneath system, not for user, so we need a real Inode as a interface for Disk Inode to route, which store its id and offset.
However, we still need one special data block which is DirEntry, as directory which store inode_number to route inode, DirEntry takes 32 bytes, so each block can store 4 DirEntry. Thus we can route to inode by &str.
Notice disk_inode contains type for dir and file. So some of inodes will store dir data and some of inodes will store file data, we can get inode of data by inode of dir.
fnfind_inode_id( &self, name: &str, disk_inode: &DiskInode, ) -> Option<u32> { // assert it is a directory assert!(disk_inode.is_dir()); let file_count = (disk_inode.size asusize) / DIRENT_SZ; letmut dirent = DirEntry::empty(); for i in0..file_count { // note assert_eq! has side effect: read data to dirent. assert_eq!( disk_inode.read_at( DIRENT_SZ * i, dirent.as_bytes_mut(), &self.block_device, ), DIRENT_SZ, ); if dirent.name() == name { returnSome(dirent.inode_number() asu32); } } None } }
Usually, the workflow of create or delete, read or write would be:
read/write
get root inode which is dir type
read/write closure of disk inode through root inode
resize specified inode
create/clear
allocation/deallocation: alloc/dealloc inode by bitmap and get its index
initialization/clear by get its block cache by its index