From 77e0ffb9816d9e6f498b3b4a8fbb8d72236725c4 Mon Sep 17 00:00:00 2001 From: Zach Brown Date: Wed, 10 Aug 2016 15:18:45 -0700 Subject: [PATCH] scoutfs: track data blocks in bmap items Up to this point we'd been storing file data in large fixed size items. This obviously needed to change to get decent large file IO patterns. This wires the file IO into the usual page cache and buffer head paths so that we write data blocks into allocations referenced by btree items. We're aggressively trying to find the highest ratio of performance to implementation complexity. Writing dirty metadata blocks during transaction commit changes a bit. We need to discover if we have dirty blocks before trying to sync the inodes. We add our _block_has_dirty() function back and use it to avoid write attempts during transaction commit. Signed-off-by: Zach Brown --- kmod/src/block.c | 27 +- kmod/src/block.h | 1 + kmod/src/filerw.c | 648 ++++++++++++++++++++++++++++++++++------------ kmod/src/filerw.h | 2 + kmod/src/format.h | 20 +- kmod/src/super.h | 5 + kmod/src/trans.c | 46 +++- 7 files changed, 561 insertions(+), 188 deletions(-) diff --git a/kmod/src/block.c b/kmod/src/block.c index e6ddf0a8..4f82eae9 100644 --- a/kmod/src/block.c +++ b/kmod/src/block.c @@ -280,8 +280,6 @@ static void block_write_end_io(struct buffer_head *bh, int uptodate) * be written again in the next transaction commit. * * Reads can traverse the blocks while they're in flight. - * - * The number of blocks written is returned, or -errno on error. */ int scoutfs_block_write_dirty(struct super_block *sb) { @@ -290,13 +288,11 @@ int scoutfs_block_write_dirty(struct super_block *sb) struct rb_node *node; struct blk_plug plug; unsigned long flags; - int count; - int err; + int ret; atomic_set(&sbi->block_writes, 1); sbi->block_write_err = 0; - count = 0; - err = 0; + ret = 0; blk_start_plug(&plug); @@ -308,7 +304,6 @@ int scoutfs_block_write_dirty(struct super_block *sb) spin_unlock_irqrestore(&sbi->block_lock, flags); atomic_inc(&sbi->block_writes); - count++; scoutfs_block_set_crc(bh); /* @@ -321,10 +316,10 @@ int scoutfs_block_write_dirty(struct super_block *sb) lock_buffer(bh); bh->b_end_io = block_write_end_io; - err = submit_bh(WRITE, bh); /* doesn't actually fail? */ + ret = submit_bh(WRITE, bh); /* doesn't actually fail? */ spin_lock_irqsave(&sbi->block_lock, flags); - if (err) + if (ret) break; } spin_unlock_irqrestore(&sbi->block_lock, flags); @@ -335,10 +330,18 @@ int scoutfs_block_write_dirty(struct super_block *sb) atomic_dec(&sbi->block_writes); wait_event(sbi->block_wq, atomic_read(&sbi->block_writes) == 0); - trace_printk("err %d sbi err %d count %d\n", - err, sbi->block_write_err, count); + trace_printk("ret %d\n", ret); + return ret; +} - return err ?: sbi->block_write_err ?: count; +/* + * The caller knows that it's not racing with writers. + */ +int scoutfs_block_has_dirty(struct super_block *sb) +{ + struct scoutfs_sb_info *sbi = SCOUTFS_SB(sb); + + return !RB_EMPTY_ROOT(&sbi->block_dirty_tree); } /* diff --git a/kmod/src/block.h b/kmod/src/block.h index 8bb2a136..ee38b677 100644 --- a/kmod/src/block.h +++ b/kmod/src/block.h @@ -13,6 +13,7 @@ struct buffer_head *scoutfs_block_dirty_alloc(struct super_block *sb); struct buffer_head *scoutfs_block_dirty_ref(struct super_block *sb, struct scoutfs_block_ref *ref); +int scoutfs_block_has_dirty(struct super_block *sb); int scoutfs_block_write_dirty(struct super_block *sb); void scoutfs_block_set_crc(struct buffer_head *bh); diff --git a/kmod/src/filerw.c b/kmod/src/filerw.c index df946ee3..7ef5ea2a 100644 --- a/kmod/src/filerw.c +++ b/kmod/src/filerw.c @@ -13,8 +13,11 @@ #include #include #include +#include +#include #include "format.h" +#include "super.h" #include "inode.h" #include "key.h" #include "filerw.h" @@ -24,173 +27,512 @@ #include "ioctl.h" /* - * File data is stored in items just like everything else. This is very - * easy to implement but incurs a copying overhead. We'll see how - * expensive that gets. + * scoutfs uses simple fixed size block mapping items to map aligned + * groups of logical file data blocks to physical block locations. * - * By making the max item size a bit less than the block size we can - * still have room for the block header which gets us file data - * checksums. File item key offsets are multiples of this max block - * size though items can be smaller if the data is sparse. This lets us - * do lookups for specific keys and take advantage of the bloom filters. + * The small block size is set to the smallest supported page size. + * This means that our file IO code never has to worry about the + * situation where a page write is smaller than the block size. We + * never have to perform RMW of blocks larger than pages, nor do we have + * to punch a whole and worry about block tracking items that could be + * sharing references to a block on either side of a smaller dirty page. + * We can simply use the kernel's buffer head code, loathed though it + * is, and have a 1:1 relationship between block writes and block + * mapping item entries. * - * This is a minimal first pass and will need more work. It'll need to - * worry about enospc in writepage and cluster access for a start. + * Dirty blocks are only written to free space. The first time a block + * hits write_page in a transaction it gets a newly allocated block. We + * get decent contiguous allocations by having per-task preallocation + * streams. These are trimmed back as the transaction is committed. We + * don't bother worrying about small transactions. + * + * Because we only write to allocated space we can't naively use the + * buffer head get_blocks support functions. They assume that they can + * write dirty buffers to existing clean mappings which is absolutely + * not true for us. We clear mappings for clean pages before we call + * block_write_begin() so that it won't write to blocks that were caned + * from previous reads. We make sure that the page is uptodate ourself + * so that it won't use readpage to read the existing block and then + * turn around and write to it. + * + * Data blocks aren't pinned for the duration of the transaction. They + * can be written out and read back in and redirtied during the lifetime + * of a transaction. As we map dirty pages we see if its current allocation + * is newly allocated in the transaction and can reuse it. + * + * XXX + * - need to wire up dirty inode? + * - enforce writing to free blknos + * - per-task allocation regions + * - tear down dirty blocks left by write errors on unmount + * - should invalidate dirty blocks if freed + * - data block checksumming (stable pages) + * - mmap creating dirty unmapped pages at writepage + * - pack small tails into inline items + * - direct IO */ -/* -* Track the intersection of the logical region of a file with a page -* and file data item. -*/ -struct data_region { - u64 item_key; - unsigned int page_off; - unsigned short len; - unsigned short item_off; -}; /* - * Map the file offset to its intersection with the page and item region. - * Returns false if the byte position is outside the page. -*/ -static bool map_data_region(struct data_region *dr, u64 pos, struct page *page) -{ - if (pos >> PAGE_SHIFT != page->index) - return false; - - dr->page_off = pos & ~PAGE_MASK; - - dr->item_off = do_div(pos, SCOUTFS_MAX_ITEM_LEN); - dr->item_key = pos; - - dr->len = min_t(int, SCOUTFS_MAX_ITEM_LEN - dr->item_off, - PAGE_SIZE - dr->page_off); - - return true; -} - -#define for_each_data_region(dr, page, pos) \ - for (pos = (u64)page->index << PAGE_SHIFT; \ - map_data_region(dr, pos, page); pos += (dr)->len) - -/* - * Copy the contents of file data items into the page. If we don't - * find an item then we zero that region of the page. + * trace_printk() doesn't support %c? * - * XXX i_size? - * XXX async? + * 1 - 1ocked + * a - uptodAte + * d - Dirty + * b - writeBack + * e - Error */ -static int scoutfs_readpage(struct file *file, struct page *page) +#define page_hexflag(page, name, val, shift) \ + (Page##name(page) ? (val << (shift * 4)) : 0) + +#define page_hexflags(page) \ + (page_hexflag(page, Locked, 0x1, 4) | \ + page_hexflag(page, Uptodate, 0xa, 3) | \ + page_hexflag(page, Dirty, 0xd, 2) | \ + page_hexflag(page, Writeback, 0xb, 1) | \ + page_hexflag(page, Error, 0xe, 0)) + +#define PGF "page %p [index %lu flags %x]" +#define PGA(page) \ + (page), (page)->index, page_hexflags(page) \ + +#define BHF "bh %p [blocknr %llu size %zu state %lx]" +#define BHA(bh) \ + (bh), (u64)(bh)->b_blocknr, (bh)->b_size, (bh)->b_state \ + +/* + * For now this is super cheesy. We just have one allocation on the + * super that is consumed as buffered writes make their way through unmapped + * buffer heads and alloc in get_block. + */ +static int alloc_file_block(struct super_block *sb, u64 *blkno) { - struct inode *inode = file->f_mapping->host; - DECLARE_SCOUTFS_BTREE_CURSOR(curs); - struct super_block *sb = inode->i_sb; - struct scoutfs_key key; - struct data_region dr; - int ret = 0; - void *addr; - u64 pos; + struct scoutfs_sb_info *sbi = SCOUTFS_SB(sb); + u64 alloc_blkno; + int order = 0; + int ret; - for_each_data_region(&dr, page, pos) { - scoutfs_set_key(&key, scoutfs_ino(inode), SCOUTFS_DATA_KEY, - dr.item_key); + *blkno = 0; - scoutfs_btree_release(&curs); - ret = scoutfs_btree_lookup(sb, &key, &curs); - if (ret == -ENOENT) { - addr = kmap_atomic(page); - memset(addr + dr.page_off, 0, dr.len); - kunmap_atomic(addr); - continue; + spin_lock(&sbi->file_alloc_lock); + + if (sbi->file_alloc_count == 0) { + spin_unlock(&sbi->file_alloc_lock); + + order = scoutfs_buddy_alloc(sb, &alloc_blkno, + SCOUTFS_BUDDY_ORDERS - 1); + if (order < 0) { + ret = order; + goto out; } - if (ret) - break; - addr = kmap_atomic(page); - memcpy(addr + dr.page_off, curs.val + dr.item_off, dr.len); - kunmap_atomic(addr); + spin_lock(&sbi->file_alloc_lock); + + if (sbi->file_alloc_count == 0) { + sbi->file_alloc_blkno = alloc_blkno; + sbi->file_alloc_count = 1 << order; + order = -1; + } } - scoutfs_btree_release(&curs); + if (sbi->file_alloc_count) { + *blkno = sbi->file_alloc_blkno; + sbi->file_alloc_blkno++; + sbi->file_alloc_count--; + ret = 0; + } else { + ret = -ENOSPC; + } - if (!ret) - SetPageUptodate(page); - unlock_page(page); + spin_unlock(&sbi->file_alloc_lock); + + if (order > 0) + scoutfs_buddy_free(sb, alloc_blkno, order); + +out: + trace_printk("allocated blkno %llu ret %d\n", *blkno, ret); return ret; } /* - * Copy the contents of the page into file items. Data integrity syncs - * will later write the dirty segment to the device. - * -* XXX zeroing regions of data items? -* XXX wbc counters? -* XXX reserve space so dirty item doesn't get enospc -- our "delalloc"? -*/ -static int scoutfs_writepage(struct page *page, struct writeback_control *wbc) + * The caller didn't need an allocated file block after all. We return + * it to the pool. This has to succeed because it's called after we've + * done things that would be annoying to revert. + */ +static void return_file_block(struct super_block *sb, u64 blkno) +{ + struct scoutfs_sb_info *sbi = SCOUTFS_SB(sb); + + spin_lock(&sbi->file_alloc_lock); + + BUG_ON(sbi->file_alloc_count && + sbi->file_alloc_blkno != (blkno + 1)); + + if (sbi->file_alloc_count == 0) + sbi->file_alloc_blkno = blkno + 1; + + sbi->file_alloc_blkno--; + sbi->file_alloc_count++; + + spin_unlock(&sbi->file_alloc_lock); +} + +/* + * The caller ensures that this is serialized against all other callers + * and writers. + */ +void scoutfs_filerw_free_alloc(struct super_block *sb) +{ + struct scoutfs_sb_info *sbi = SCOUTFS_SB(sb); + + trace_printk("blkno %llu count %llu\n", sbi->file_alloc_blkno, + sbi->file_alloc_count); + + if (sbi->file_alloc_count) + scoutfs_buddy_free_extent(sb, sbi->file_alloc_blkno, + sbi->file_alloc_count); + + sbi->file_alloc_blkno = 0; + sbi->file_alloc_count = 0; +} + +static void set_bmap_key(struct scoutfs_key *key, struct inode *inode, + u64 iblock) +{ + scoutfs_set_key(key, scoutfs_ino(inode), SCOUTFS_BMAP_KEY, + iblock >> SCOUTFS_BLOCK_MAP_SHIFT); +} + +/* + * Return the number of contiguously mapped blocks starting from the + * given logical block in the inode. We only return the number + * contained in one block map item. We walk through more items if it + * makes a difference. + */ +static int contig_mapped_blocks(struct inode *inode, u64 iblock, u64 *blkno) { - struct inode *inode = page->mapping->host; - DECLARE_SCOUTFS_BTREE_CURSOR(curs); struct super_block *sb = inode->i_sb; + DECLARE_SCOUTFS_BTREE_CURSOR(curs); + struct scoutfs_block_map *bmap; struct scoutfs_key key; - struct data_region dr; - void *addr; - u64 pos; + int ret; + int i; + + *blkno = 0; + + set_bmap_key(&key, inode, iblock); + ret = scoutfs_btree_lookup(sb, &key, &curs); + if (!ret) { + bmap = curs.val; + + i = iblock & SCOUTFS_BLOCK_MAP_MASK; + *blkno = le64_to_cpu(bmap->blkno[i]); + + while (i < SCOUTFS_BLOCK_MAP_COUNT && bmap->blkno[i]) { + ret++; + i++; + } + scoutfs_btree_release(&curs); + } else if (ret == -ENOENT) { + ret = 0; + } + + trace_printk("ino %llu iblock %llu blkno %llu ret %d\n", + scoutfs_ino(inode), iblock, *blkno, ret); + + return ret; +} + +/* + * Make sure that the mapped block at the given logical block number is + * writable in this transaction. If it's not we allocate and reference + * a new block. If there was a previous stable block we free it. We + * give the caller the writable block number. + * + * Writeback is allowed during a transaction so we can get here with + * buffer heads that are newly allocated and being written to but for + * blocks that were allocated in the current transacation. In that + * case we re-use the existing mapping. None of it will be stable until + * there's a sync that writes all the referencing metadata. + */ +static int map_writable_block(struct inode *inode, u64 iblock, u64 *blkno_ret) +{ + struct super_block *sb = inode->i_sb; + DECLARE_SCOUTFS_BTREE_CURSOR(curs); + struct scoutfs_block_map *bmap; + struct scoutfs_key key; + bool inserted = false; + u64 old_blkno = 0; + u64 new_blkno = 0; + int ret; + int err; + int i; + + set_bmap_key(&key, inode, iblock); + + /* we always need a writable block map item */ + ret = scoutfs_btree_update(sb, &key, &curs); + if (ret < 0 && ret != -ENOENT) + goto out; + + /* might need to create a new item and delete it after errors */ + if (ret == -ENOENT) { + ret = scoutfs_btree_insert(sb, &key, sizeof(*bmap), &curs); + if (ret < 0) + goto out; + memset(curs.val, 0, sizeof(*bmap)); + inserted = true; + } + + bmap = curs.val; + i = iblock & SCOUTFS_BLOCK_MAP_MASK; + old_blkno = le64_to_cpu(bmap->blkno[i]); + + /* + * If the existing block was free in stable then its dirty in + * this trans and we can use it. + */ + if (old_blkno) { + ret = scoutfs_buddy_was_free(sb, old_blkno, 0); + if (ret < 0) + goto out; + if (ret > 0) { + *blkno_ret = old_blkno; + ret = 0; + goto out; + } + } + + ret = alloc_file_block(sb, &new_blkno); + if (ret < 0) + goto out; + + if (old_blkno) { + ret = scoutfs_buddy_free(sb, old_blkno, 0); + if (ret) + goto out; + } + + bmap->blkno[i] = cpu_to_le64(new_blkno); + *blkno_ret = new_blkno; + new_blkno = 0; + ret = 0; +out: + scoutfs_btree_release(&curs); + if (ret) { + if (new_blkno) + return_file_block(sb, new_blkno); + if (inserted) { + err = scoutfs_btree_delete(sb, &key); + BUG_ON(err); /* always succeeds */ + } + } + + return ret; +} + +static int scoutfs_readpage_get_block(struct inode *inode, sector_t iblock, + struct buffer_head *bh, int create) +{ + u64 blkno; int ret; - set_page_writeback(page); + if (WARN_ON_ONCE(create)) + return -EINVAL; + + ret = contig_mapped_blocks(inode, iblock, &blkno); + if (ret > 0) { + map_bh(bh, inode->i_sb, blkno); + bh->b_size = min_t(int, bh->b_size, ret << inode->i_blkbits); + ret = 0; + } + + trace_printk("ino %llu iblock %llu create %d "BHF"\n", + scoutfs_ino(inode), (u64)iblock, create, BHA(bh)); + + return ret; +} + +static int scoutfs_readpage(struct file *file, struct page *page) +{ + trace_printk(PGF"\n", PGA(page)); + + return mpage_readpage(page, scoutfs_readpage_get_block); +} + +static int scoutfs_readpages(struct file *file, struct address_space *mapping, + struct list_head *pages, unsigned nr_pages) +{ + return mpage_readpages(mapping, pages, nr_pages, + scoutfs_readpage_get_block); +} + +/* + * For now we don't know what to do if unmapped blocks make it to + * writepage (mmap?). + */ +static int scoutfs_writepage_get_block(struct inode *inode, sector_t iblock, + struct buffer_head *bh, int create) +{ + trace_printk("ino %llu iblock %llu create %d "BHF"\n", + scoutfs_ino(inode), (u64)iblock, create, BHA(bh)); + + return WARN_ON_ONCE(-EINVAL); +} + +/* + * Dirty file blocks can be written to their newly allocated free blocks + * at any time. They won't be referenced by metadata until the current + * transaction is committed. They can be re-read and re-dirtied at + * their free block number in this transaction. + */ +static int scoutfs_writepage(struct page *page, struct writeback_control *wbc) +{ + trace_printk(PGF"\n", PGA(page)); + + return block_write_full_page(page, scoutfs_writepage_get_block, wbc); +} + +static int scoutfs_writepages(struct address_space *mapping, + struct writeback_control *wbc) +{ + trace_printk("mapping %p\n", mapping); + + return mpage_writepages(mapping, wbc, scoutfs_writepage_get_block); +} + +/* + * Block allocation during buffered writes needs to make sure that the + * dirty block will be written to free space. + */ +static int scoutfs_write_begin_get_block(struct inode *inode, sector_t iblock, + struct buffer_head *bh, int create) +{ + u64 blkno = 0; + int ret; + + if (WARN_ON_ONCE(!create)) + return -EINVAL; + + ret = map_writable_block(inode, iblock, &blkno); + if (ret == 0) { + map_bh(bh, inode->i_sb, blkno); + bh->b_size = SCOUTFS_BLOCK_SIZE; + ret = 0; + } + + trace_printk("ino %llu iblock %llu create %d ret %d "BHF"\n", + scoutfs_ino(inode), (u64)iblock, create, ret, BHA(bh)); + return ret; +} + +/* XXX could make a for_each wrapper if we get a few of these */ +static inline void clear_mapped_page_buffers(struct page *page) +{ + struct buffer_head *head; + struct buffer_head *bh; + + if (!page_has_buffers(page)) + return; + + head = page_buffers(page); + bh = head; + do { + if (buffer_mapped(bh)) { + trace_printk(BHF"\n", BHA(bh)); + clear_buffer_mapped(bh); + } + + bh = bh->b_this_page; + } while (bh != head); +} + +/* + * Dirty blocks have to be mapped to be written out to free space so + * that we don't overwrite live data. We're relying on + * block_write_begin() to call get_block(). There are two problems with + * this. + * + * First, if it's going to be trying to read a partial block before writing + * then we can't give it the location to read. It'll just mark the + * block dirty and write to that same location. We use readpage to make + * the page uptodate if it's going to be satisfying a partial overwrite. + * + * Second, we can't let it use mappings that were used by readpage to + * read the current stable data. We need to have get_block be called + * for existing clean uptodate pages so that we can reallocate them to + * free space. We do this by clearing the buffer mappings for every buffer + * on the page for every call. This is probably unnecessarily expensive + * because we don't need to do it for clean buffers. That optimization + * would need to be done very carefully. + */ +static int scoutfs_write_begin(struct file *file, + struct address_space *mapping, loff_t pos, + unsigned len, unsigned flags, + struct page **pagep, void **fsdata) +{ + struct inode *inode = mapping->host; + struct super_block *sb = inode->i_sb; + pgoff_t index = pos >> PAGE_SHIFT; + struct page *page; + int ret; + +retry: + page = grab_cache_page_write_begin(mapping, index, flags); + if (!page) + return -ENOMEM; + + trace_printk(PGF" pos %llu len %u\n", PGA(page), (u64)pos, len); + + /* + * read in the page if we're going to be dirtying part of the + * page. readpage catches when this is a read past i_size or + * from a hole and zeros the buffer. + */ + if (!PageUptodate(page) && !IS_ALIGNED(pos | len, SCOUTFS_BLOCK_SIZE)) { + ClearPageError(page); + ret = scoutfs_readpage(NULL, page); + if (ret) { + page_cache_release(page); + goto out; + } + + wait_on_page_locked(page); + if (!PageUptodate(page)) { + page_cache_release(page); + ret = -EIO; + goto out; + } + + /* let grabbing deal with weird page states */ + page_cache_release(page); + goto retry; + } ret = scoutfs_hold_trans(sb); if (ret) goto out; - for_each_data_region(&dr, page, pos) { - scoutfs_set_key(&key, scoutfs_ino(inode), SCOUTFS_DATA_KEY, - dr.item_key); + /* can't re-enter fs, have trans */ + flags |= AOP_FLAG_NOFS; - /* XXX dirty */ - scoutfs_btree_release(&curs); - ret = scoutfs_btree_insert(sb, &key, SCOUTFS_MAX_ITEM_LEN, - &curs); - if (ret) - break; + /* make sure our get_block gets a chance to alloc */ + clear_mapped_page_buffers(page); - addr = kmap_atomic(page); - memcpy(curs.val + dr.item_off, addr + dr.page_off, dr.len); - kunmap_atomic(addr); - - } - - scoutfs_btree_release(&curs); - scoutfs_release_trans(sb); + ret = __block_write_begin(page, pos, len, + scoutfs_write_begin_get_block); out: - if (ret) { - SetPageError(page); - mapping_set_error(&inode->i_data, ret); - } + trace_printk(PGF" pos %llu len %u ret %d\n", + PGA(page), (u64)pos, len, ret); + if (ret < 0) { + /* XXX handle truncating? */ + unlock_page(page); + put_page(page); + page = NULL; + } - end_page_writeback(page); - unlock_page(page); - - return ret; -} - -static int scoutfs_write_begin(struct file *file, struct address_space *mapping, - loff_t pos, unsigned len, unsigned flags, - struct page **pagep, void **fsdata) -{ - struct inode *inode = mapping->host; - pgoff_t index = pos >> PAGE_CACHE_SHIFT; - struct page *page; - - trace_scoutfs_write_begin(scoutfs_ino(inode), pos, len); - - page = grab_cache_page_write_begin(mapping, index, flags); - if (!page) - return -ENOMEM; - - *pagep = page; - return 0; + *pagep = page; + return ret; } static int scoutfs_write_end(struct file *file, struct address_space *mapping, @@ -199,45 +541,21 @@ static int scoutfs_write_end(struct file *file, struct address_space *mapping, { struct inode *inode = mapping->host; struct super_block *sb = inode->i_sb; - unsigned off; + int ret; - trace_scoutfs_write_end(scoutfs_ino(inode), pos, len, copied); + trace_printk("ino %llu "PGF" pos %llu len %u copied %d\n", + scoutfs_ino(inode), PGA(page), (u64)pos, len, copied); - off = pos & (PAGE_CACHE_SIZE - 1); - - /* zero the stale part of the page if we did a short copy */ - if (copied < len) - zero_user_segment(page, off + copied, len); - - if (pos + copied > inode->i_size) { - i_size_write(inode, pos + copied); - - /* - * XXX This is a crazy hack that will go away when the - * file data paths are more robust. We're barely - * holding them together with duct tape while building - * up the robust metadata support that's needed to do a - * good job with the data pats. - */ - if (!scoutfs_hold_trans(sb)) { - if (!scoutfs_dirty_inode_item(inode)) - scoutfs_update_inode_item(inode); - scoutfs_release_trans(sb); - } - } - - if (!PageUptodate(page)) - SetPageUptodate(page); - set_page_dirty(page); - unlock_page(page); - page_cache_release(page); - - return copied; + ret = generic_write_end(file, mapping, pos, len, copied, page, fsdata); + scoutfs_release_trans(sb); + return ret; } const struct address_space_operations scoutfs_file_aops = { .readpage = scoutfs_readpage, + .readpages = scoutfs_readpages, .writepage = scoutfs_writepage, + .writepages = scoutfs_writepages, .write_begin = scoutfs_write_begin, .write_end = scoutfs_write_end, }; diff --git a/kmod/src/filerw.h b/kmod/src/filerw.h index 2d9d478e..c182349d 100644 --- a/kmod/src/filerw.h +++ b/kmod/src/filerw.h @@ -4,4 +4,6 @@ extern const struct address_space_operations scoutfs_file_aops; extern const struct file_operations scoutfs_file_fops; +void scoutfs_filerw_free_alloc(struct super_block *sb); + #endif diff --git a/kmod/src/format.h b/kmod/src/format.h index 2a3605c4..fbd09ddd 100644 --- a/kmod/src/format.h +++ b/kmod/src/format.h @@ -100,7 +100,7 @@ struct scoutfs_key { #define SCOUTFS_INODE_KEY 1 #define SCOUTFS_XATTR_KEY 2 #define SCOUTFS_DIRENT_KEY 3 -#define SCOUTFS_DATA_KEY 4 +#define SCOUTFS_BMAP_KEY 4 #define SCOUTFS_MAX_ITEM_LEN 512 @@ -244,4 +244,22 @@ struct scoutfs_xattr { __u8 name[0]; } __packed; +/* + * We use simple block map items to map a aligned fixed group of logical + * block offsets to physical blocks. We make them a decent size to + * reduce the item storage overhead per block referenced, but we don't + * want them so large that they start to take up an extraordinary amount + * of space for small files. 8 block items ranges from around 3% to .3% + * overhead for files that use only one or all of the blocks in the + * mapping item. + */ +#define SCOUTFS_BLOCK_MAP_SHIFT 3 +#define SCOUTFS_BLOCK_MAP_COUNT (1 << SCOUTFS_BLOCK_MAP_SHIFT) +#define SCOUTFS_BLOCK_MAP_MASK (SCOUTFS_BLOCK_MAP_COUNT - 1) + +struct scoutfs_block_map { + __le32 crc[SCOUTFS_BLOCK_MAP_COUNT]; + __le64 blkno[SCOUTFS_BLOCK_MAP_COUNT]; +}; + #endif diff --git a/kmod/src/super.h b/kmod/src/super.h index 7c3617b4..455f397e 100644 --- a/kmod/src/super.h +++ b/kmod/src/super.h @@ -43,6 +43,11 @@ struct scoutfs_sb_info { struct kset *kset; struct scoutfs_counters *counters; + + /* XXX we'd like this to be per task, not per super */ + spinlock_t file_alloc_lock; + u64 file_alloc_blkno; + u64 file_alloc_count; }; static inline struct scoutfs_sb_info *SCOUTFS_SB(struct super_block *sb) diff --git a/kmod/src/trans.c b/kmod/src/trans.c index 3291b83c..9be98ede 100644 --- a/kmod/src/trans.c +++ b/kmod/src/trans.c @@ -15,15 +15,17 @@ #include #include #include +#include #include "super.h" #include "block.h" #include "trans.h" #include "buddy.h" +#include "filerw.h" #include "scoutfs_trace.h" /* - * scoutfs metadata blocks are written in atomic transactions. + * scoutfs blocks are written in atomic transactions. * * Writers hold transactions to dirty blocks. The transaction can't be * written until these active writers release the transaction. We don't @@ -44,11 +46,24 @@ */ /* - * It's critical that this not try to perform IO if there's nothing - * dirty. The sync at unmount can have this work scheduled after sync - * returns and the unmount path starts to tear down supers and block - * devices. We have to safely detect that there's nothing to do using - * nothing in the vfs. + * This work func is responsible for writing out all the dirty blocks + * that make up the current dirty transaction. It prevents writers from + * holding a transaction so it doesn't have to worry about blocks being + * dirtied while it is working. + * + * Any dirty block had to have allocated a new blkno which would have + * created dirty allocator metadata blocks. We can avoid writing + * entirely if we don't have any dirty metadata blocks. This is + * important because we don't try to serialize this work during + * unmount.. we can execute as the vfs is shutting down.. we need to + * decide that nothing is dirty without calling the vfs at all. + * + * We first try to sync the dirty inodes and write their dirty data blocks, + * then we write all our dirty metadata blocks, and only when those succeed + * do we write the new super that references all of these newly written blocks. + * + * If there are write errors then blocks are kept dirty in memory and will + * be written again at the next sync. */ void scoutfs_trans_write_func(struct work_struct *work) { @@ -57,15 +72,26 @@ void scoutfs_trans_write_func(struct work_struct *work) struct super_block *sb = sbi->sb; bool advance = false; int ret = 0; + bool have_umount; wait_event(sbi->trans_hold_wq, atomic_cmpxchg(&sbi->trans_holds, 0, -1) == 0); - /* XXX probably want to write out dirty pages in inodes */ + if (scoutfs_block_has_dirty(sb)) { + /* XXX need writeback errors from inode address spaces? */ - ret = scoutfs_block_write_dirty(sb); - if (ret > 0) { - ret = scoutfs_write_dirty_super(sb); + /* XXX definitely don't understand this */ + have_umount = down_read_trylock(&sb->s_umount); + + sync_inodes_sb(sb); + + if (have_umount) + up_read(&sb->s_umount); + + scoutfs_filerw_free_alloc(sb); + + ret = scoutfs_block_write_dirty(sb) ?: + scoutfs_write_dirty_super(sb); if (!ret) advance = 1; }