[PATCH 3/3] f2fs: add ioctl to force out-place-update per file
From: Jeuk Kim
Date: Fri Aug 28 2026 - 03:22:03 EST
Out-place-update can only be enabled for an entire filesystem, via the
"mode=lfs" mount option or the ipu_policy sysfs entry. There is no way
to force OPU on a single file. Add F2FS_OPU_FL and
F2FS_IOC_{SET,GET}_OPU_FILE to control OPU on a per-file basis.
Reject OPU for pinned, swap, and device-alias files because they require
stable block addresses. The flag forces OPU without enabling LFS mode,
so block allocation may still use SSR.
The flag is kept in the on-disk i_flags word, so it survives umount and
inode eviction. An older kernel preserves the bit but ignores it and
applies its normal update policy to the file.
Signed-off-by: Jeuk Kim <jeuk20.kim@xxxxxxxxxxx>
---
fs/f2fs/data.c | 25 ++++++++++----
fs/f2fs/f2fs.h | 9 ++++-
fs/f2fs/file.c | 69 +++++++++++++++++++++++++++++++++++++--
include/uapi/linux/f2fs.h | 2 ++
4 files changed, 95 insertions(+), 10 deletions(-)
diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c
index 618e6fc2da0c..2babfcf1dae5 100644
--- a/fs/f2fs/data.c
+++ b/fs/f2fs/data.c
@@ -1639,12 +1639,13 @@ int f2fs_map_blocks(struct inode *inode, struct f2fs_map_blocks *map, int flag)
unsigned int start_pgofs;
int bidx = 0;
bool is_hole;
- bool lfs_dio_write;
+ bool opu_dio_write;
if (!maxblocks)
return 0;
- lfs_dio_write = (flag == F2FS_GET_BLOCK_DIO && f2fs_should_opu(inode) &&
+ opu_dio_write = (flag == F2FS_GET_BLOCK_DIO &&
+ f2fs_should_opu(inode) &&
map->m_may_create);
if (!map->m_may_create && f2fs_map_blocks_cached(inode, map, flag)) {
@@ -1685,7 +1686,7 @@ int f2fs_map_blocks(struct inode *inode, struct f2fs_map_blocks *map, int flag)
next_dnode:
if (map->m_may_create) {
- if (f2fs_lfs_mode(sbi))
+ if (f2fs_lfs_mode(sbi) || f2fs_is_opu_file(inode))
f2fs_balance_fs(sbi, true);
f2fs_map_lock(sbi, &lc, flag);
}
@@ -1715,9 +1716,10 @@ int f2fs_map_blocks(struct inode *inode, struct f2fs_map_blocks *map, int flag)
goto sync_out;
}
- /* use out-place-update for direct IO under LFS mode */
+ /* use out-place-update for direct IO under LFS mode or on OPU files */
if (map->m_may_create && (is_hole ||
- (flag == F2FS_GET_BLOCK_DIO && f2fs_should_opu(inode) &&
+ (flag == F2FS_GET_BLOCK_DIO &&
+ f2fs_should_opu(inode) &&
map->m_last_pblk != blkaddr))) {
if (unlikely(f2fs_cp_error(sbi))) {
err = -EIO;
@@ -1804,13 +1806,13 @@ int f2fs_map_blocks(struct inode *inode, struct f2fs_map_blocks *map, int flag)
if (map->m_multidev_dio)
map->m_bdev = FDEV(bidx).bdev;
- if (lfs_dio_write)
+ if (opu_dio_write)
map->m_last_pblk = NULL_ADDR;
} else if (map_is_mergeable(sbi, map, blkaddr, flag, bidx, ofs)) {
ofs++;
map->m_len++;
} else {
- if (lfs_dio_write)
+ if (opu_dio_write)
map->m_last_pblk = blkaddr;
goto sync_out;
}
@@ -2862,6 +2864,9 @@ bool f2fs_should_update_inplace(struct inode *inode, struct f2fs_io_info *fio)
if (f2fs_is_pinned_file(inode))
return true;
+ if (f2fs_is_opu_file(inode))
+ return false;
+
/* if this is cold file, we should overwrite to avoid fragmentation */
if (file_is_cold(inode) && !is_inode_flag_set(inode, FI_OPU_WRITE))
return true;
@@ -2899,6 +2904,9 @@ bool f2fs_should_update_outplace(struct inode *inode, struct f2fs_io_info *fio)
if (is_inode_flag_set(inode, FI_OPU_WRITE))
return true;
+ if (f2fs_is_opu_file(inode))
+ return true;
+
if (fio) {
if (page_private_gcing(fio->page))
return true;
@@ -4357,6 +4365,9 @@ static int f2fs_swap_activate(struct swap_info_struct *sis, struct file *file,
if (f2fs_readonly(sbi->sb))
return -EROFS;
+ if (f2fs_is_opu_file(inode))
+ return -EINVAL;
+
if (f2fs_lfs_mode(sbi) && !f2fs_sb_has_blkzoned(sbi)) {
f2fs_err(sbi, "Swapfile not supported in LFS mode");
return -EINVAL;
diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h
index e85d4db3a401..79f3f365432a 100644
--- a/fs/f2fs/f2fs.h
+++ b/fs/f2fs/f2fs.h
@@ -3322,6 +3322,7 @@ static inline void f2fs_change_bit(unsigned int nr, char *addr)
#define F2FS_NOCOMP_FL 0x00000400 /* Don't compress */
#define F2FS_INDEX_FL 0x00001000 /* hash-indexed directory */
#define F2FS_DIRSYNC_FL 0x00010000 /* dirsync behaviour (directories only) */
+#define F2FS_OPU_FL 0x01000000 /* File always uses out-place-update */
#define F2FS_PROJINHERIT_FL 0x20000000 /* Create with parents projid */
#define F2FS_CASEFOLD_FL 0x40000000 /* Casefolded file */
#define F2FS_DEVICE_ALIAS_FL 0x80000000 /* File for aliasing a device */
@@ -3598,6 +3599,11 @@ static inline bool f2fs_is_pinned_file(struct inode *inode)
return is_inode_flag_set(inode, FI_PIN_FILE);
}
+static inline bool f2fs_is_opu_file(struct inode *inode)
+{
+ return F2FS_I(inode)->i_flags & F2FS_OPU_FL;
+}
+
static inline bool f2fs_is_atomic_file(struct inode *inode)
{
return is_inode_flag_set(inode, FI_ATOMIC_FILE);
@@ -4944,7 +4950,8 @@ static inline bool f2fs_should_opu(struct inode *inode)
if (f2fs_is_pinned_file(inode))
return false;
- return f2fs_lfs_mode(F2FS_I_SB(inode));
+ return f2fs_lfs_mode(F2FS_I_SB(inode)) ||
+ f2fs_is_opu_file(inode);
}
static inline bool f2fs_is_sequential_zone_area(struct f2fs_sb_info *sbi,
diff --git a/fs/f2fs/file.c b/fs/f2fs/file.c
index 37d4e5c8dbcb..09731ad05d70 100644
--- a/fs/f2fs/file.c
+++ b/fs/f2fs/file.c
@@ -3558,6 +3558,9 @@ static int f2fs_ioc_set_pin_file(struct file *filp, unsigned long arg)
goto done;
} else if (f2fs_is_pinned_file(inode)) {
goto done;
+ } else if (f2fs_is_opu_file(inode)) {
+ ret = -EINVAL;
+ goto out;
}
if (F2FS_HAS_BLOCKS(inode)) {
@@ -3606,6 +3609,62 @@ static int f2fs_ioc_get_pin_file(struct file *filp, unsigned long arg)
return put_user(pin, (u32 __user *)arg);
}
+static int f2fs_ioc_set_opu_file(struct file *filp, unsigned long arg)
+{
+ struct inode *inode = file_inode(filp);
+ struct f2fs_sb_info *sbi = F2FS_I_SB(inode);
+ struct f2fs_inode_info *fi = F2FS_I(inode);
+ __u32 opu;
+ int ret;
+
+ if (get_user(opu, (__u32 __user *)arg))
+ return -EFAULT;
+
+ if (!S_ISREG(inode->i_mode))
+ return -EINVAL;
+
+ if (f2fs_readonly(sbi->sb))
+ return -EROFS;
+
+ ret = mnt_want_write_file(filp);
+ if (ret)
+ return ret;
+
+ inode_lock(inode);
+
+ if (!opu) {
+ /*
+ * Direct writes issued while the flag was set may still be in
+ * flight with their blocks unwritten, so drain them before
+ * f2fs_file_read_iter() stops serializing against them.
+ */
+ if (f2fs_is_opu_file(inode))
+ inode_dio_wait(inode);
+ fi->i_flags &= ~F2FS_OPU_FL;
+ goto done;
+ }
+
+ if (f2fs_is_pinned_file(inode) || IS_DEVICE_ALIASING(inode)) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ fi->i_flags |= F2FS_OPU_FL;
+done:
+ f2fs_mark_inode_dirty_sync(inode, true);
+ f2fs_update_time(sbi, REQ_TIME);
+out:
+ inode_unlock(inode);
+ mnt_drop_write_file(filp);
+ return ret;
+}
+
+static int f2fs_ioc_get_opu_file(struct file *filp, unsigned long arg)
+{
+ return put_user(f2fs_is_opu_file(file_inode(filp)) ? 1 : 0,
+ (__u32 __user *)arg);
+}
+
static int f2fs_ioc_get_dev_alias_file(struct file *filp, unsigned long arg)
{
return put_user(IS_DEVICE_ALIASING(file_inode(filp)) ? 1 : 0,
@@ -4740,6 +4799,10 @@ static long __f2fs_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
return f2fs_ioc_get_dev_alias_file(filp, arg);
case F2FS_IOC_IO_PRIO:
return f2fs_ioc_io_prio(filp, arg);
+ case F2FS_IOC_SET_OPU_FILE:
+ return f2fs_ioc_set_opu_file(filp, arg);
+ case F2FS_IOC_GET_OPU_FILE:
+ return f2fs_ioc_get_opu_file(filp, arg);
default:
return -ENOTTY;
}
@@ -4936,8 +4999,8 @@ static ssize_t f2fs_file_read_iter(struct kiocb *iocb, struct iov_iter *to)
dio = f2fs_should_use_dio(inode, iocb, to);
- /* In LFS mode, if there is inflight dio, wait for its completion */
- if (f2fs_lfs_mode(F2FS_I_SB(inode)) &&
+ /* For OPU direct writes, wait for any inflight dio to complete */
+ if ((f2fs_lfs_mode(F2FS_I_SB(inode)) || f2fs_is_opu_file(inode)) &&
get_pages(F2FS_I_SB(inode), F2FS_DIO_WRITE) &&
(!f2fs_is_pinned_file(inode) || !dio))
inode_dio_wait(inode);
@@ -5522,6 +5585,8 @@ long f2fs_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
case F2FS_IOC_COMPRESS_FILE:
case F2FS_IOC_GET_DEV_ALIAS_FILE:
case F2FS_IOC_IO_PRIO:
+ case F2FS_IOC_SET_OPU_FILE:
+ case F2FS_IOC_GET_OPU_FILE:
break;
default:
return -ENOIOCTLCMD;
diff --git a/include/uapi/linux/f2fs.h b/include/uapi/linux/f2fs.h
index 795e26258355..ebfef7e88bcb 100644
--- a/include/uapi/linux/f2fs.h
+++ b/include/uapi/linux/f2fs.h
@@ -45,6 +45,8 @@
#define F2FS_IOC_START_ATOMIC_REPLACE _IO(F2FS_IOCTL_MAGIC, 25)
#define F2FS_IOC_GET_DEV_ALIAS_FILE _IOR(F2FS_IOCTL_MAGIC, 26, __u32)
#define F2FS_IOC_IO_PRIO _IOW(F2FS_IOCTL_MAGIC, 27, __u32)
+#define F2FS_IOC_SET_OPU_FILE _IOW(F2FS_IOCTL_MAGIC, 28, __u32)
+#define F2FS_IOC_GET_OPU_FILE _IOR(F2FS_IOCTL_MAGIC, 29, __u32)
/*
* should be same as XFS_IOC_GOINGDOWN.
--
2.43.0