Commit ef3d0fd2 authored by Andi Kleen's avatar Andi Kleen Committed by root

vfs: do (nearly) lockless generic_file_llseek

The i_mutex lock use of generic _file_llseek hurts.  Independent processes
accessing the same file synchronize over a single lock, even though
they have no need for synchronization at all.

Under high utilization this can cause llseek to scale very poorly on larger
systems.

This patch does some rethinking of the llseek locking model:

First the 64bit f_pos is not necessarily atomic without locks
on 32bit systems. This can already cause races with read() today.
This was discussed on linux-kernel in the past and deemed acceptable.
The patch does not change that.

Let's look at the different seek variants:

SEEK_SET: Doesn't really need any locking.
If there's a race one writer wins, the other loses.

For 32bit the non atomic update races against read()
stay the same. Without a lock they can also happen
against write() now.  The read() race was deemed
acceptable in past discussions, and I think if it's
ok for read it's ok for write too.

=> Don't need a lock.

SEEK_END: This behaves like SEEK_SET plus it reads
the maximum size too. Reading the maximum size would have the
32bit atomic problem. But luckily we already have a way to read
the maximum size without locking (i_size_read), so we
can just use that instead.

Without i_mutex there is no synchronization with write() anymore,
however since the write() update is atomic on 64bit it just behaves
like another racy SEEK_SET.  On non atomic 32bit it's the same
as SEEK_SET.

=> Don't need a lock, but need to use i_size_read()

SEEK_CUR: This has a read-modify-write race window
on the same file. One could argue that any application
doing unsynchronized seeks on the same file is already broken.
But for the sake of not adding a regression here I'm
using the file->f_lock to synchronize this. Using this
lock is much better than the inode mutex because it doesn't
synchronize between processes.

=> So still need a lock, but can use a f_lock.

This patch implements this new scheme in generic_file_llseek.
I dropped generic_file_llseek_unlocked and changed all callers.
Signed-off-by: default avatarAndi Kleen <ak@linux.intel.com>
Signed-off-by: default avatarChristoph Hellwig <hch@lst.de>
parent 847cc637
...@@ -1821,7 +1821,7 @@ static loff_t btrfs_file_llseek(struct file *file, loff_t offset, int origin) ...@@ -1821,7 +1821,7 @@ static loff_t btrfs_file_llseek(struct file *file, loff_t offset, int origin)
switch (origin) { switch (origin) {
case SEEK_END: case SEEK_END:
case SEEK_CUR: case SEEK_CUR:
offset = generic_file_llseek_unlocked(file, offset, origin); offset = generic_file_llseek(file, offset, origin);
goto out; goto out;
case SEEK_DATA: case SEEK_DATA:
case SEEK_HOLE: case SEEK_HOLE:
......
...@@ -723,7 +723,7 @@ static loff_t cifs_llseek(struct file *file, loff_t offset, int origin) ...@@ -723,7 +723,7 @@ static loff_t cifs_llseek(struct file *file, loff_t offset, int origin)
if (rc < 0) if (rc < 0)
return (loff_t)rc; return (loff_t)rc;
} }
return generic_file_llseek_unlocked(file, offset, origin); return generic_file_llseek(file, offset, origin);
} }
static int cifs_setlease(struct file *file, long arg, struct file_lock **lease) static int cifs_setlease(struct file *file, long arg, struct file_lock **lease)
......
...@@ -63,11 +63,11 @@ static loff_t gfs2_llseek(struct file *file, loff_t offset, int origin) ...@@ -63,11 +63,11 @@ static loff_t gfs2_llseek(struct file *file, loff_t offset, int origin)
error = gfs2_glock_nq_init(ip->i_gl, LM_ST_SHARED, LM_FLAG_ANY, error = gfs2_glock_nq_init(ip->i_gl, LM_ST_SHARED, LM_FLAG_ANY,
&i_gh); &i_gh);
if (!error) { if (!error) {
error = generic_file_llseek_unlocked(file, offset, origin); error = generic_file_llseek(file, offset, origin);
gfs2_glock_dq_uninit(&i_gh); gfs2_glock_dq_uninit(&i_gh);
} }
} else } else
error = generic_file_llseek_unlocked(file, offset, origin); error = generic_file_llseek(file, offset, origin);
return error; return error;
} }
......
...@@ -198,11 +198,12 @@ static loff_t nfs_file_llseek(struct file *filp, loff_t offset, int origin) ...@@ -198,11 +198,12 @@ static loff_t nfs_file_llseek(struct file *filp, loff_t offset, int origin)
if (retval < 0) if (retval < 0)
return (loff_t)retval; return (loff_t)retval;
/* AK: should drop this lock. Unlikely to be needed. */
spin_lock(&inode->i_lock); spin_lock(&inode->i_lock);
loff = generic_file_llseek_unlocked(filp, offset, origin); loff = generic_file_llseek(filp, offset, origin);
spin_unlock(&inode->i_lock); spin_unlock(&inode->i_lock);
} else } else
loff = generic_file_llseek_unlocked(filp, offset, origin); loff = generic_file_llseek(filp, offset, origin);
return loff; return loff;
} }
......
...@@ -35,23 +35,45 @@ static inline int unsigned_offsets(struct file *file) ...@@ -35,23 +35,45 @@ static inline int unsigned_offsets(struct file *file)
return file->f_mode & FMODE_UNSIGNED_OFFSET; return file->f_mode & FMODE_UNSIGNED_OFFSET;
} }
static loff_t lseek_execute(struct file *file, struct inode *inode,
loff_t offset, loff_t maxsize)
{
if (offset < 0 && !unsigned_offsets(file))
return -EINVAL;
if (offset > maxsize)
return -EINVAL;
if (offset != file->f_pos) {
file->f_pos = offset;
file->f_version = 0;
}
return offset;
}
/** /**
* generic_file_llseek_unlocked - lockless generic llseek implementation * generic_file_llseek - generic llseek implementation for regular files
* @file: file structure to seek on * @file: file structure to seek on
* @offset: file offset to seek to * @offset: file offset to seek to
* @origin: type of seek * @origin: type of seek
* *
* Updates the file offset to the value specified by @offset and @origin. * This is a generic implemenation of ->llseek usable for all normal local
* Locking must be provided by the caller. * filesystems. It just updates the file offset to the value specified by
* @offset and @origin under i_mutex.
*
* Synchronization:
* SEEK_SET is unsynchronized (but atomic on 64bit platforms)
* SEEK_CUR is synchronized against other SEEK_CURs, but not read/writes.
* read/writes behave like SEEK_SET against seeks.
* SEEK_END
*/ */
loff_t loff_t
generic_file_llseek_unlocked(struct file *file, loff_t offset, int origin) generic_file_llseek(struct file *file, loff_t offset, int origin)
{ {
struct inode *inode = file->f_mapping->host; struct inode *inode = file->f_mapping->host;
switch (origin) { switch (origin) {
case SEEK_END: case SEEK_END:
offset += inode->i_size; offset += i_size_read(inode);
break; break;
case SEEK_CUR: case SEEK_CUR:
/* /*
...@@ -62,14 +84,22 @@ generic_file_llseek_unlocked(struct file *file, loff_t offset, int origin) ...@@ -62,14 +84,22 @@ generic_file_llseek_unlocked(struct file *file, loff_t offset, int origin)
*/ */
if (offset == 0) if (offset == 0)
return file->f_pos; return file->f_pos;
offset += file->f_pos; /*
break; * f_lock protects against read/modify/write race with other
* SEEK_CURs. Note that parallel writes and reads behave
* like SEEK_SET.
*/
spin_lock(&file->f_lock);
offset = lseek_execute(file, inode, file->f_pos + offset,
inode->i_sb->s_maxbytes);
spin_unlock(&file->f_lock);
return offset;
case SEEK_DATA: case SEEK_DATA:
/* /*
* In the generic case the entire file is data, so as long as * In the generic case the entire file is data, so as long as
* offset isn't at the end of the file then the offset is data. * offset isn't at the end of the file then the offset is data.
*/ */
if (offset >= inode->i_size) if (offset >= i_size_read(inode))
return -ENXIO; return -ENXIO;
break; break;
case SEEK_HOLE: case SEEK_HOLE:
...@@ -77,46 +107,13 @@ generic_file_llseek_unlocked(struct file *file, loff_t offset, int origin) ...@@ -77,46 +107,13 @@ generic_file_llseek_unlocked(struct file *file, loff_t offset, int origin)
* There is a virtual hole at the end of the file, so as long as * There is a virtual hole at the end of the file, so as long as
* offset isn't i_size or larger, return i_size. * offset isn't i_size or larger, return i_size.
*/ */
if (offset >= inode->i_size) if (offset >= i_size_read(inode))
return -ENXIO; return -ENXIO;
offset = inode->i_size; offset = i_size_read(inode);
break; break;
} }
if (offset < 0 && !unsigned_offsets(file)) return lseek_execute(file, inode, offset, inode->i_sb->s_maxbytes);
return -EINVAL;
if (offset > inode->i_sb->s_maxbytes)
return -EINVAL;
/* Special lock needed here? */
if (offset != file->f_pos) {
file->f_pos = offset;
file->f_version = 0;
}
return offset;
}
EXPORT_SYMBOL(generic_file_llseek_unlocked);
/**
* generic_file_llseek - generic llseek implementation for regular files
* @file: file structure to seek on
* @offset: file offset to seek to
* @origin: type of seek
*
* This is a generic implemenation of ->llseek useable for all normal local
* filesystems. It just updates the file offset to the value specified by
* @offset and @origin under i_mutex.
*/
loff_t generic_file_llseek(struct file *file, loff_t offset, int origin)
{
loff_t rval;
mutex_lock(&file->f_dentry->d_inode->i_mutex);
rval = generic_file_llseek_unlocked(file, offset, origin);
mutex_unlock(&file->f_dentry->d_inode->i_mutex);
return rval;
} }
EXPORT_SYMBOL(generic_file_llseek); EXPORT_SYMBOL(generic_file_llseek);
......
...@@ -964,7 +964,12 @@ struct file { ...@@ -964,7 +964,12 @@ struct file {
#define f_dentry f_path.dentry #define f_dentry f_path.dentry
#define f_vfsmnt f_path.mnt #define f_vfsmnt f_path.mnt
const struct file_operations *f_op; const struct file_operations *f_op;
spinlock_t f_lock; /* f_ep_links, f_flags, no IRQ */
/*
* Protects f_ep_links, f_flags, f_pos vs i_size in lseek SEEK_CUR.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
#ifdef CONFIG_SMP #ifdef CONFIG_SMP
int f_sb_list_cpu; int f_sb_list_cpu;
#endif #endif
...@@ -2398,8 +2403,6 @@ file_ra_state_init(struct file_ra_state *ra, struct address_space *mapping); ...@@ -2398,8 +2403,6 @@ file_ra_state_init(struct file_ra_state *ra, struct address_space *mapping);
extern loff_t noop_llseek(struct file *file, loff_t offset, int origin); extern loff_t noop_llseek(struct file *file, loff_t offset, int origin);
extern loff_t no_llseek(struct file *file, loff_t offset, int origin); extern loff_t no_llseek(struct file *file, loff_t offset, int origin);
extern loff_t generic_file_llseek(struct file *file, loff_t offset, int origin); extern loff_t generic_file_llseek(struct file *file, loff_t offset, int origin);
extern loff_t generic_file_llseek_unlocked(struct file *file, loff_t offset,
int origin);
extern int generic_file_open(struct inode * inode, struct file * filp); extern int generic_file_open(struct inode * inode, struct file * filp);
extern int nonseekable_open(struct inode * inode, struct file * filp); extern int nonseekable_open(struct inode * inode, struct file * filp);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment