Re: [PATCH 1/3] ext4: Add EXT4_IOC_TRUNCATE_BLOCK_RANGE ioctl
From: Andreas Dilger <hidden>
Date: 2013-06-24 01:55:09
Also in:
linux-fsdevel, lkml
On 2013-06-23, at 0:07, Namjae Jeon [off-list ref] wrote:
From: Namjae Jeon <redacted> The EXT4_IOC_TRUNCATE_BLOCK_RANGE removes the data blocks lying between [start, "start + length") and updates the logical block numbers of data blocks starting from "start + length" block to last block of file. This will maintain contiguous nature of logical block numbers after block removal. Both the inode's disksize and logical size are updated after block removal
I don't think "truncate" describes this operation very well. It is more like "punch hole and shrink size". The real question I have for both this operation is what practical use it has. I don't think that "editing a movie clip" is a real example, because the stream will not align on block boundaries, and will just result in copying most of the file data if it is a byte-aligned operation. Cheers, Andreas
quoted hunk ↗ jump to hunk
Signed-off-by: Namjae Jeon <redacted> Signed-off-by: Ashish Sangwan <redacted> --- fs/ext4/ext4.h | 8 ++ fs/ext4/ext4_extents.h | 3 + fs/ext4/extents.c | 245 ++++++++++++++++++++++++++++++++++++++++++++++++ fs/ext4/ioctl.c | 62 ++++++++++++ 4 files changed, 318 insertions(+)diff --git a/fs/ext4/ext4.h b/fs/ext4/ext4.h index 6ed348d..df2c411 100644 --- a/fs/ext4/ext4.h +++ b/fs/ext4/ext4.h@@ -590,6 +590,7 @@ enum {#define EXT4_IOC_MOVE_EXT _IOWR('f', 15, struct move_extent) #define EXT4_IOC_RESIZE_FS _IOW('f', 16, __u64) #define EXT4_IOC_SWAP_BOOT _IO('f', 17) +#define EXT4_IOC_TRUNCATE_BLOCK_RANGE _IOW('f', 18, struct truncate_range) #if defined(__KERNEL__) && defined(CONFIG_COMPAT) /*@@ -682,6 +683,11 @@ struct move_extent { __u64 moved_len; /* moved block length */}; +struct truncate_range { + __u32 start_block; + __u32 length; +}; + #define EXT4_EPOCH_BITS 2 #define EXT4_EPOCH_MASK ((1 << EXT4_EPOCH_BITS) - 1) #define EXT4_NSEC_MASK (~0UL << EXT4_EPOCH_BITS)@@ -2692,6 +2698,8 @@ extern int ext4_find_delalloc_range(struct inode *inode,extern int ext4_find_delalloc_cluster(struct inode *inode, ext4_lblk_t lblk); extern int ext4_fiemap(struct inode *inode, struct fiemap_extent_info *fieinfo, __u64 start, __u64 len); +extern int ext4_ext_truncate_range(struct inode *inode, ext4_lblk_t start, + ext4_lblk_t end, ext4_lblk_t last_block); /* move_extent.c */diff --git a/fs/ext4/ext4_extents.h b/fs/ext4/ext4_extents.h index 51bc821..cc113cc 100644 --- a/fs/ext4/ext4_extents.h +++ b/fs/ext4/ext4_extents.h@@ -178,6 +178,9 @@ struct ext4_ext_path {#define EXT_MAX_INDEX(__hdr__) \ (EXT_FIRST_INDEX((__hdr__)) + le16_to_cpu((__hdr__)->eh_max) - 1) +#define EXTENT_START_FLAG 0x1 +#define INDEX_START_FLAG 0x2 + static inline struct ext4_extent_header *ext_inode_hdr(struct inode *inode) { return (struct ext4_extent_header *) EXT4_I(inode)->i_data;diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c index 937593e..ed85e34 100644 --- a/fs/ext4/extents.c +++ b/fs/ext4/extents.c@@ -4757,3 +4757,248 @@ int ext4_fiemap(struct inode *inode, struct fiemap_extent_info *fieinfo, return error;} + +/* + * ext4_trange_dirty_path: Function to mark the path buffer dirty. + * It also checks if there are sufficient credits left in the + * journal to update metadata. If the number of credits are less + * restart the handle with additional credits. + * + * @handle: journal handle + * @inode: file inode + * @path: pointer to path + * @num: number of inodes to be updated + * + * Returns: 0 on success or negative value on error + */ +int ext4_trange_dirty_path(handle_t *handle, struct inode *inode, + struct ext4_ext_path *path, + int num, ...) +{ + int credits, err, i; + struct inode *iptr; + va_list args; + + /* + * Check if need to extend journal credits + * 3 for leaf, sb, and inode plus 2 (bmap and group + * descriptor) for each block group; assume two block + * groups + */ + if (handle->h_buffer_credits < 7*(num + 1)) { + credits = ext4_writepage_trans_blocks(inode); + va_start(args, num); + for (i = 1; i <= num; i++) { + iptr = va_arg(args, struct inode *); + credits += ext4_writepage_trans_blocks(iptr); + } + va_end(args); + err = ext4_ext_truncate_extend_restart(handle, inode, credits); + /* EAGAIN is success */ + if (err && err != -EAGAIN) + return err; + } + err = ext4_ext_get_access(handle, inode, path); + return err; +} + +/* + * ext4_ext_update_path: update the extents of a path structure + * lying between path[depth].p_ext and EXT_LAST_EXTENT(path[depth].p_hdr) + * subtracting shift from starting block for each extent. + * + * @path: path for which extents are updated + * @shift: Number of blocks to be subtracted from first logical block + * that extent covers for each extent. + * @inode: file inode + * @handle: journal handle + * @start_block: Points to the starting block of next extent which is + * to be updated. + * + * Returns: 0 on success or negative on error. + */ +int ext4_ext_update_path(struct ext4_ext_path *path, ext4_lblk_t shift, + struct inode *inode, handle_t *handle, + ext4_lblk_t *start_block) +{ + int depth, err = 0, flag = 0; + struct ext4_extent *ex_start, *ex_last; + + depth = path->p_depth; + while (depth >= 0) { + if (depth == path->p_depth) { + ex_start = path[depth].p_ext; + if (!ex_start) + return -EIO; + + err = ext4_trange_dirty_path(handle, inode, + path + depth, 0); + if (err) + goto out; + + if (path[depth].p_ext == + EXT_FIRST_EXTENT(path[depth].p_hdr)) + flag |= EXTENT_START_FLAG; + + ex_last = EXT_LAST_EXTENT(path[depth].p_hdr); + while (ex_start <= ex_last) { + *start_block = ex_start->ee_block + + ext4_ext_get_actual_len(ex_start); + ex_start->ee_block -= shift; + ex_start++; + } + err = ext4_ext_dirty(handle, inode, path + depth); + if (err) + goto out; + } else { + /* If encountered starting extent, update index too */ + if (path->p_depth - depth == 1) { + if (flag & EXTENT_START_FLAG) { + /* Update index too */ + err = ext4_trange_dirty_path(handle, + inode, path + depth, 0); + if (err) + goto out; + path[depth].p_idx->ei_block -= shift; + err = ext4_ext_dirty(handle, inode, + path + depth); + if (err) + goto out; + flag &= ~EXTENT_START_FLAG; + } else + /* No need to update any extent index */ + break; + } + /* Check, if earlier encountered starting index */ + if (flag & INDEX_START_FLAG) { + err = ext4_trange_dirty_path(handle, inode, + path + (depth), 0); + if (err) + goto out; + path[depth].p_idx->ei_block -= shift; + err = ext4_ext_dirty(handle, inode, + path + depth); + if (err) + goto out; + flag &= ~INDEX_START_FLAG; + } + /* Check if this is a starting index */ + if (path[depth].p_idx == + EXT_FIRST_INDEX(path[depth].p_hdr)) { + /* starting of a block */ + flag |= INDEX_START_FLAG; + } else + break; + } + depth--; + } +out: + return err; +} + +/* + * ext4_ext_update_logical: update logical blocks ranging from start + * to the end block for inode by moving them shift blocks to the left + * + * @inode: file inode + * @handle: journal handle + * @start_block : starting block for block updation + * @shift: number of blocks to be shifted + * @end_block: last block to be updated + * + * Returns: 0 on success or negative on failure + */ +static int ext4_ext_update_logical(struct inode *inode, handle_t *handle, + ext4_lblk_t start_block, ext4_lblk_t shift, + ext4_lblk_t end_block) +{ + struct ext4_ext_path *path; + int err = 0; + + while (start_block < end_block) { + path = ext4_ext_find_extent(inode, start_block, NULL); + if (IS_ERR(path)) { + err = PTR_ERR(path); + break; + } + err = ext4_ext_update_path(path, shift, inode, + handle, &start_block); + ext4_ext_drop_refs(path); + kfree(path); + if (err) + break; + } + return err; +} + +/* + * ext4_ext_truncate_range: truncate the block range from start + * block to end block including the end block from inode. + * + * @inode: file inode + * @start: start block + * @end: end block + * last_block: last_block number of the inode + * + * Returns: 0 on success or negative on error + */ +int ext4_ext_truncate_range(struct inode *inode, ext4_lblk_t start, + ext4_lblk_t end, ext4_lblk_t last_block) +{ + int ret, credits; + ext4_lblk_t shift = end - start + 1; + handle_t *handle; + loff_t isize_reduced; + int blkbits = inode->i_blkbits; + struct address_space *mapping = inode->i_mapping; + + /* sync dirty pages for transfer */ + if (mapping->nrpages && mapping_tagged(mapping, PAGECACHE_TAG_DIRTY)) { + ret = filemap_write_and_wait_range(mapping, + (loff_t)start << blkbits, + ((loff_t)(last_block + 1) << blkbits) - 1); + if (ret) + return ret; + } + truncate_inode_pages_range(inode->i_mapping, + start << inode->i_blkbits, -1); + ext4_inode_block_unlocked_dio(inode); + inode_dio_wait(inode); + down_write(&EXT4_I(inode)->i_data_sem); + ext4_discard_preallocations(inode); + ret = ext4_es_remove_extent(inode, start, end - start + 1); + if (ret) + goto out; + + credits = ext4_writepage_trans_blocks(inode); + handle = ext4_journal_start(inode, EXT4_HT_TRUNCATE, credits); + if (IS_ERR(handle)) { + ret = PTR_ERR(handle); + goto out; + } + + ret = ext4_ext_remove_space(inode, start, end); + if (ret) + goto journal_stop; + + ext4_discard_preallocations(inode); + + if (end < last_block) { + ret = ext4_ext_update_logical(inode, handle, end + 1, + shift, last_block + 1); + if (ret) + goto journal_stop; + } + isize_reduced = (loff_t)shift << blkbits; + i_size_write(inode, inode->i_size - isize_reduced); + EXT4_I(inode)->i_disksize -= isize_reduced; + inode->i_mtime = inode->i_ctime = ext4_current_time(inode); + ext4_mark_inode_dirty(handle, inode); +journal_stop: + ext4_journal_stop(handle); +out: + ext4_inode_resume_unlocked_dio(inode); + up_write(&EXT4_I(inode)->i_data_sem); + return ret; +} +diff --git a/fs/ext4/ioctl.c b/fs/ext4/ioctl.c index 9491ac0..0530daf 100644 --- a/fs/ext4/ioctl.c +++ b/fs/ext4/ioctl.c@@ -622,6 +622,68 @@ resizefs_out: return 0; } + case EXT4_IOC_TRUNCATE_BLOCK_RANGE: + { + struct truncate_range tr; + ext4_lblk_t last_block, end_block; + int error; + loff_t i_size = i_size_read(inode); + + if (!i_size) + return 0; + + if (!(filp->f_mode & FMODE_WRITE)) + return -EBADF; + + if (IS_IMMUTABLE(inode) || IS_APPEND(inode)) + return -EPERM; + + if (!S_ISREG(inode->i_mode)) + return -EOPNOTSUPP; + + if (IS_SWAPFILE(inode)) + return -EOPNOTSUPP; + + if (!ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS)) + return -EOPNOTSUPP; + + if (EXT4_HAS_RO_COMPAT_FEATURE(sb, + EXT4_FEATURE_RO_COMPAT_BIGALLOC)) { + ext4_msg(sb, KERN_ERR, + "Truncate block range not supported with bigalloc"); + return -EOPNOTSUPP; + } + + if (copy_from_user(&tr, (const void *) arg, + sizeof(struct truncate_range))) + return -EFAULT; + + if (!tr.length) + return -EINVAL; + + end_block = tr.start_block + tr.length - 1; + + last_block = ((round_up(i_size, + EXT4_BLOCK_SIZE(inode->i_sb))) + >> inode->i_blkbits) - 1; + if (tr.start_block > end_block || + tr.start_block > last_block) + return -EINVAL; + + if (end_block > last_block) + end_block = last_block; + + error = mnt_want_write_file(filp); + if (error) + return error; + + mutex_lock(&inode->i_mutex); + error = ext4_ext_truncate_range(inode, tr.start_block, + end_block, last_block); + mutex_unlock(&inode->i_mutex); + mnt_drop_write_file(filp); + return error; + } default: return -ENOTTY;-- 1.7.9.5