- William Smith [email protected]
- John Seong [email protected]
- Gera Groshev [email protected]
- Joe Kuang [email protected]
Create cache.c
and cache.h
in pintos/src/filesys
#define CACHE_NUM_ENTRIES 64
in cache.c
#define CACHE_NUM_CHANCES 1
in cache.c
struct cache_block {
struct lock cache_block_lock;
uint8_t data[BLOCK_SECTOR_SIZE];
block_sector_t disk_sector_index;
bool valid; // false if cache_block contains garbage
bool dirty;
size_t chances_remaining;
};
struct inode {
struct list_elem elem;
block_sector_t sector;
int open_cnt;
bool removed;
int deny_write_cnt;
};
static struct cache_block cache[CACHE_NUM_ENTRIES];
static struct lock cache_update_lock;
void filesys_init(bool format)
- Call
cache_init()
void cache_init(void)
- call
lock_init(&cache_update_lock)
- for each
struct cache_block
incache
:- set
valid
member to false - call
lock_init()
oncache_block_lock
member
- set
int cache_evict(block_sector_t sector_index)
- declare and initialize
static size_t clock_position = 0
- acquire
cache_update_lock
- before checking values of any
cache_block
, acquire the lock for thecache_block
first - iterate through
cache
to make suresector_index
is not incache
- if
sector_index
in cache then releasecache_update_lock
and return(-index) - 1
forcache
entry index - Perform clock algorithm, loop forever:
- first check the
valid
member and advance clock and returncache
entry index ifvalid
- next check
chances_remaining
== 0 and break - release
cache_block
lock and advanceclock_position
- first check the
- release
cache_update_lock
- if
dirty
callblock_write()
, setvalid
to false, and returncache
entry index
void cache_read(struct block *fs_device, block_sector_t sector_index, void *destination, off_t offset, int chunk_size)
- before checking values of any
cache_block
, acquire the lock for thecache_block
first - iterate through
cache
to check ifsector_index
is incache
, if so:- save cache index of hit → i
- reset
chances_remaining
to CACHE_NUM_CHANCES memcpy()
chunk_size
bytes todestination
fromcache[i].data
starting atoffset
- release
cache_block_lock
- if
sector_index
is not incache
, callcache_evict()
- if
cache_evict()
returns positive index i:cache_replace()
thecache
entry at the index - else
cache_evict()
returns negative index, it was a hit, so convert index toi = -(index + 1)
- reset
chances_remaining
to CACHE_NUM_CHANCES memcpy()
chunk_size
bytes todestination
fromcache[i].data
starting atoffset
- release
cache_block_lock
- if
void cache_write(struct block *fs_device, block_sector_t sector_index, void *source, off_t offset, int chunk_size)
- before checking values of any
cache_block
, acquire the lock for thecache_block
first - iterate through
cache
to check ifsector_index
is incache
, if so:- save cache index of hit → i
- set
dirty
to true - reset
chances_remaining
to CACHE_NUM_CHANCES memcpy()
chunk_size
bytes tocache[i].data
starting atoffset
fromsource
- release
cache_block_lock
- if
sector_index
is not incache
, callcache_evict()
- if
cache_evict()
returns positive index i:cache_replace()
thecache
entry at index i - else
cache_evict()
returns negative index, it was a hit, so convert index toi = -(index + 1)
- set
dirty
to true - reset
chances_remaining
to CACHE_NUM_CHANCES memcpy()
chunk_size
bytes tocache[i].data
starting atoffset
fromsource
- release
cache_block_lock
- if
void cache_replace(int index, sector_index)
- ASSERT thread is holding lock
- set valid to true
- set dirty to false
- set
disk_sector_index
tosector_index
- set
chances_remaining
toCACHE_NUM_CHANCES
- call
block_read (fs_device, sector_index, &(cache[index].data))
block_read()
andblock_write()
will be replaced withcache_read()
andcache_write()
in all following functions so that all reads and writes go through cache.inode_create ()
inode_read_at ()
inode_write_at ()
partition_read()
inpartition.c
read_partition_table()
inpartition.c
fsutil_extract
infsutil.c
- The
bounce
buffer will be removed ininode_read_at ()
andinode_write_at ()
. Instead of the if-else block,cache_read(fs_device, sector_idx, buffer + bytes_read, sector_ofs, chunk_size)
will be called ininode_read_at ()
andcache_write(fs_device, sector_idx, buffer + bytes_written, sector_ofs, chunk_size)
will be called ininode_write_at ()
.
- Remove
struct inode_disk data
The rationale here is that we do not want to incur the cache penalty of storing this metadata on memory, as this is both inefficient and would limit the number of files that can be opened simultaneously to 64. Instead, we can get the data by reading the block sector of member variable
start
in the inode struct, which will return the inode_disk corresponding to that inode. We can then get the data from here directly.
static block_sector_t byte_to_sector (const struct inode *inode, off_t pos)
- declare and malloc
struct inode_disk *data
- read from cache into
*data
- replace
inode->data
with*data
and save values in*data
to temporary variable before returning - free
*data
before returning
struct inode *inode_open (block_sector_t sector)
- remove
block_read (fs_device, inode->sector, &inode->data)
void inode_close (struct inode *inode)
- declare and malloc
struct inode_disk *data
- read from cache into
*data
- replace
inode->data
with*data
and save values in*data
to temporary variable before returning - free
*data
before returning
off_t inode_length (const struct inode *inode)
- declare and malloc
struct inode_disk *data
- read from cache into
*data
- replace
inode->data
with*data
anddata->length
to temporary variable before returning - free
*data
before returning
Synchronization for task 1 requires the acquisition of locks for each cache_block before checking or modifying its data. We created a
cache_block_lock
for that. In addition, thecache_evict()
will acquire thecache_update_lock
before modifying the cache.
By having a lock for each cache block, we can synchronize access between reading and writing. The
cache_update_lock
and doing another pass to make sure there really is a miss incache_evict()
accounts for the edge case where if two threads end up with a cache miss for the same disk sector, they shouldn’t both be evicting from the cache, otherwise there would be duplicates.cache_evict()
will be the only function that updates the cache and the only place wherecache_update_lock
is acquired. This allows simultaneous access to read and write to the cache while an entry is being evicted and individualcache_block_lock
will keep the individual reads and writes synchronized. Now that data must go through the cache,block_read()
andblock_write()
are not called directly, but instead the cache is checked first and the cache takes care of reading to and writing from the disk device.
Adding syscall for
inumber
is moved to task 3 documentation
#define DIRECT_BLOCK_COUNT 124
#define INDIRECT_BLOCK_COUNT 128
struct inode_disk {
block_sector_t direct_blocks[DIRECT_BLOCK_COUNT];
block_sector_t indirect_block;
block_sector_t doubly_indirect_block;
off_t length;
unsigned magic;
};
Initial
struct inode_disk
containsblock_sector_t start
variable that indicates the first data sector anduint32_t unused[125]
which is used as a pad to make the size ofstruct inode_disk
equal to 512 bytes. Current design doesn’t allow extension of a file since a file is consisted of consecutive block sectors. In order to allow a file extension, we use direct, indirect, and doubly indirect blocks to store file and such blocks’ sector numbers are stored in thestruct inode_disk
.
There will be 124
direct_blocks
, 1indirect_block
, and 1doubly_indirect_block
pointers due to 512 bytes constraint. Theindirect_block
will point to 128 blocks(128 x 4 = 512)
anddoubly_indirect_block
will point to 128indirect_block
, resulting128 * 128
blocks. This totals to124 + 128 + 128 * 128 = 16636
sectors, which approximates to 8.52 MB.
struct indirect_block {
block_sector_t blocks[INDIRECT_BLOCK_COUNT];
};
static bool inode_allocate (struct inode_disk *inode_disk, off_t length);
static void inode_deallocate (struct inode *inode);
bool inode_create (block_sector_t sector, off_t length)
- Instead of calling
free_map_allocate ()
to allocate sectors, callinode_allocate ()
- Since
struct inode_disk
only requires 1 sector to contain the whole file due to direct, indirect, and doubly-indirect block pointers, we only callcache_write ()
once oninode_disk
instead of each sector the file is occupying.
static bool inode_allocate (struct inode_disk *inode_disk, off_t length)
inode_allocate ()
will be called during two instances, once duringinode_create ()
and another time duringinode_write_at ()
when you are writing pastEOF
- Blocks will be allocated in the order of
direct
->indirect
->doubly-indirect
depending on how much spaces are needed - For calling
inode_allocate ()
to create a file, first calculate number of sectors needed by callingbytes_to_sectors ()
. - Starting from the
direct_blocks
, allocate the blocks one by one by callingfree_map_allocate ()
. - When there are more sectors needed to be allocated after depleting
direct_blocks
, allocatestruct indirect_block
. struct indirect_block
will have 128 blocks. If more block sectors are needed after depletingindirect_block
, allocatedoubly-indirect
block.doubly_indirect
block will be aindirect_block
consisting 128indirect_block
pointers. Allocate amount of blocks needed.- For calling
inode_allocate ()
to extend file,inode_allocate ()
will check each sector and only allocate if it hasn’t been allocated before. This requiresoff_t length
to beoffset + size
, meaning total size that will be resulted.
void inode_close (struct inode *inode)
- Since
inode
is allocated usinginode_allocate ()
, we useinode_deallocate ()
to deallocate instead offree_map_release ()
static void inode_deallocate (struct inode *inode)
- First calculate number of block sectors occupied by calling
bytes_to_sectors (inode->data.length)
- In the order of
direct
->indirect
->doubly-indirect
, callfree_map_release ()
until all the block sectors used are freed.
static block_sector_t byte_to_sector (const struct inode *inode, off_t pos)
- Since a file is not consisted of consecutive block sectors anymore, current
byte_to_sector ()
won’t work byte_to_sector ()
will travel down to correct index of block sectors that contains byte offsetpos
within inode- In the order of
direct
->indirect
->doubly-indirect
, check whetherpos
lies within the block
Changes to inode_read_at (struct inode *inode, void *buffer_, off_t size, off_t offset)
to handle appending
- Malloc
struct inode_disk_t *metadata
- Call cache_read(), passing in metadata->data
- Check if offset > metadata->data.start + metadata->data.length
- If so, return 0 (reading past EOF should return 0 bytes)
- free metadata
off_t inode_write_at (struct inode *inode, const void *buffer_, off_t size, off_t offset)
- Support file extension
- If
byte_to_sector ()
returns -1, it means you are writing pastEOF
.- If that’s the case, call
inode_allocate (offset + size)
to allocate additional block sectors needed to accommodate space for writing to the file. - Malloc
struct inode_disk_t *metadata
- Call cache_read(), passing in metadata->data
- Compute zero_pad_count = offset - (metadata->data.start + metadata->data.length)
- If zero_pad_count > 0, calloc buffer of 0’s called
zero_padding
of sizezero_padding_size
- Call cache_write, passing in zero_padding as source, and offset = metadata->data.start, size = metadata->data.length
- If that’s the case, call
- Removal of first if statement entirely, which is implementing “write-through” behavior of the free_map.
- This works because all functions access the
free_map
, instead of thefree_map_file
, and we update thefree_map_file
periodically and during shutdown.
- Removal of call to
bitmap_write ()
, which is implementing “write-through” behavior - This works because all functions access the
free_map
, instead of thefree_map_file
, and we update thefree_map_file
periodically and during shutdown.
#define FLUSH_PERIOD <some not yet determined value>
- Iterate through the cache, acquiring and releasing each block on the way, and write each dirty page to disk.
- Wrapper for a call to
bitmap_write (free_map, free_map_file)
. - It will acquire the free_map_lock, write the current state of the free_map to persistent storage, and release the free_map lock.
- call
cache_flush ()
andfree_map_flush ()
.
- Loop infinitely doing the following:
- Call
timer_sleep(FLUSH_PERIOD);
- Call
filesys_flush()
- Call
- We call time_sleep() first so that when the thread is first created and run (which is in filesys_init), we do not flush an empty cache and empty free_map.
- call
thread_create(filesys_flush_peridocally, NULL)
to start the periodic flush thread - This must be done at the end of the function, so that we are sure the cache, inode system, and free map have already been initialized.
- In very beginning, add call to
filesys_flush ()
. - This will handle updating persistent storage of the filesystem in the case of a shutdown, reboot, or kernel panic.
- Because this is called before the call to
filesys_done ()
inshutdown ()
, we know that thefree_map
will still exist in memory, thereby preventing a crash.
- Call
lock_init (free_map_lock)
- Acquire and release the
free_map_lock
Synchronization for task 2 requires making free map related functions thread-safe. We created a lock for free_map and the lock will be acquired/released when
free_map_allocate ()
andfree_map_release ()
is called. Since free_map is being accessed as a static variable instead of as a file, it will not go through the cache, and therefore must have its own synchronization via the lock.
Having 124 direct blocks, 1 indirect block, and 1 doubly-indirect block covers just about 8 MB. Such design will result
struct inode_disk
to be exactly 512 bytes in size and sincestruct inode_disk
holds sector numbers, file extension can be done easily by allocating more sectors when needed. There will bestruct indirect-block
that will hold array of 128block_sector_t
variables but not for doubly-indirect since doubly-indirect will be an indirect-block that holds 128 indirect-blocks. Allocating and deallocating sectors in this design might be tricky and requires a bit of thorough coding, but it should still be more efficient than other approaches.
struct thread {
struct dir *working_dir;
};
// Add boolean DIR to identify a directory inode
struct inode_disk {
bool isdir;
};
bool inode_is_dir (struct inode *)
return inode->data.isdir
NEW: Returns true if inode refers to a directory
int inode_inumber (struct inode *)
return (int) inode->sector
NEW: Returns inode sector as the inumber
Adds a boolean DIR parameter to identify a directory inode
bool inode_create (block_sector_t sector, off_t length, bool isdir)
- Set
dir
boolean forinode_disk
disk_inode->isdir = isdir
// Add a lock to struct dir
struct dir {
struct lock dir_lock;
}
// NEW: Returns a struct dir that corresponds to a DIRECTORY
struct dir * dir_open_directory (char * directory)
- Tokenize directory into dir_part
- start_inode = curr_thread->working_dir OR dir_open_root()
- From start_inode, traverse each level of the directory tree through each dir_entry using dir_lookup(start_inode, dir_part, &next_inode)
- Return the last found inode if it is not removed
bool dir_create (block_sector_t sector, size_t entry_cnt)
- Reserve the first dir entry at offset 0 after creating inode
- This will be used to store the parent directory sector
Add boolean isdir to parameter to identify whether the file is a directory
bool dir_add (struct dir *dir, const char *name, block_sector_t inode_sector, bool, isdir)
- If it is a directory, update the first entry of the file struct dir to reflect the parent struct dir
Account “..” and “.” in directory name
bool dir_lookup (const struct dir *dir, const char *name, struct inode **inode)
- “..”: Check first dir entry for parent dir
- “.”: Use current dir
Add a boolean DIR parameter to identify a directory inode
bool filesys_create (const char *name, off_t initial_size, bool isdir)
- Separate directory and file name
dir = dir_open_directory ()
- Call create and add functions with
isdir
bool (forinode
anddir
structs)
struct file * filesys_open (const char *name)
- Separate directory and file name
// Add parent’s working directory to struct load_synch
struct load_synch {
Struct dir *parent_working_dir;
}
static void start_process (void *load_info_);
- Access parent’s working dir and set current thread’s working dir
void process_exit (void)
- Close current thread’s working dir
bool sys_chdir (const char *dir)
- Set
thread_current()->working_dir = dir
if it is valid.
bool sys_mkdir (const char *dir)
- Use
filesys_create()
- Directory parsing handled internally in
filesys_create
bool readdir (int fd, char *name)
struct file *file = get_file(thread_current(), fd);
struct inode *inode = file_get_inode(file)
- Use
dir_readdir()
- Check if inode is a valid directory
isdir()
bool isdir (int fd)
struct file *file = get_file(thread_current(), fd);
struct inode *inode = file_get_inode(file)
- Use
inode_isdir()
int inumber (int fd)
struct file *file = get_file(thread_current(), fd);
struct inode *inode = file_get_inode(file)
return inode_inumber()
An individual lock is associated with each dir struct. This lock will be used whenever thread safe operations is performed with the dir. As each dir is associated with a different level in the directory tree, this allows us to ensure mutual exclusion only on operations that modify the same dir.
Each tokenized part in the directory path is associated with its own inode and dir structs. By treating each level in the directory tree as a separate dir, we can recursively traverse the “tree” and simplify the logic in accessing different files/directories. Also, by reserving the first dir entry as a holder for the parent directory sector, it allows us to easily handle the “..” case in parsing the directory tree.
write behind functionality is already implemented in our project in task 2, therefore our given implementation is what we would use. Please see task 2 for details, but to summarize, we would re-implement the timer from Project 1 that does not busy wait. In filesys_init(), we would call thread_create(), passing in a helper function called filesys_flush_periodically(), which would infinitely loop, calling
timer_sleep(FLUSH_PERIOD)
, and then calling filesys_flush(), which is another function we would implement, which would iterate through the cache and write back all dirty pages to disk. In addition, we would write back our free_map (since we removed file writing of the free_map during updates, so that it has a “write-back” semantics).
For read ahead functionality, we would do the following. Implement a helper function, called inode_read_at_asynch(), which will have the same exact behavior as inode_read_at(), except with added functionality to predict the next block that the user process will want to read. As an important note, inode_read_at_asynch() cannot call inode_read_at(), despite the similar code, as doing so would cause an infinite recursion where the call to inode_read_at() calls inode_read_at_asynch(), which calls inode_read_at(), etc. In calls to inode_read_at(), we would add a call to thread_create(), where we pass in the function inode_read_at_asynch() for the child thread to execute, and passing into the *aux the necessary arguments, most notable of which would be offset. The offset should be the original offset passed into inode_read_at() + 512, so that the call to byte_to_sector() will return the next contiguous block sector in the file. Regarding the other arguments, we will malloc a buffer of size 1 that will be the destination buffer, and we will free it at the end, and the size argument will also be 1. Therefore, the function will retrieve the next block in the file and place it in the cache, and then write 1 byte into the destination buffer for ease of debugging (instead of dealing with 0’s and nulls) and subsequently free the buffer.