proc: fix a dentry lock race between release_task and lookup
PATCH 1/1 proc: fix a dentry lock race between release_task and lookup
From: Zhihao Cheng <[email protected]>
commit d919a1e79bac890421537cf02ae773007bf55e6b upstream.
Commit 7bc3e6e55acf06 ("proc: Use a list of inodes to flush from proc") moved proc_flush_task() behind __exit_signal(). Then, process systemd can take long period high cpu usage during releasing task in following concurrent processes:
systemd pskernel_waitid stat(/proc/tgid)
do_wait filename_lookup
wait_consider_task lookup_fast
release_task
__exit_signal
__unhash_process
detach_pid
__change_pid // remove task->pid_links
d_revalidate -> pid_revalidate // 0
d_invalidate(/proc/tgid)
shrink_dcache_parent(/proc/tgid)
d_walk(/proc/tgid)
spin_lock_nested(/proc/tgid/fd)
// iterating opened fd
proc_flush_pid |
d_invalidate (/proc/tgid/fd) |
shrink_dcache_parent(/proc/tgid/fd) |
shrink_dentry_list(subdirs) ↓
shrink_lock_dentry(/proc/tgid/fd) --> race on dentry lockFunction d_invalidate() will remove dentry from hash firstly, but why does proc_flush_pid() process dentry '/proc/tgid/fd' before dentry '/proc/tgid'? That's because proc_pid_make_inode() adds proc inode in reverse order by invoking hlist_add_head_rcu(). But proc should not add any inodes under '/proc/tgid' except '/proc/tgid/task/pid', fix it by adding inode into 'pid->inodes' only if the inode is /proc/tgid or /proc/tgid/task/pid.
Performance regression:
Create 200 tasks, each task open one file for 50,000 times. Kill all tasks when opened files exceed 10,000,000 (cat /proc/sys/fs/file-nr).
Before fix:
$ time killall -wq aa real 4m40.946s # During this period, we can see 'ps' and 'systemd' taking high cpu usage.
After fix:
$ time killall -wq aa real 1m20.732s # During this period, we can see 'systemd' taking high cpu usage.
--- fs/proc/base.c | 46 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 8 deletions(-)
fs/proc/base.c +38 -8
@@ -1881,7 +1881,7 @@ void proc_pid_evict_inode(struct proc_inode *ei)put_pid (pid); } -struct inode *proc_pid_make_inode (struct super_block * sb, +struct inode *proc_pid_make_inode (struct super_block *sb,struct task_struct *task,umode_t mode) {struct inode * inode;
@@ -1910,11 +1910,6 @@ struct inode *proc_pid_make_inode(struct super_block * sb,/* Let the pid remember us for quick removal */ ei->pid = pid; -if (S_ISDIR (mode)) { -spin_lock (&pid->lock); -hlist_add_head_rcu (&ei->sibling_inodes, &pid->inodes); -spin_unlock (&pid->lock); - }task_dump_owner (task,0 , &inode->i_uid, &inode->i_gid);security_task_to_inode (task, inode);
@@ -1927,6 +1922,39 @@ struct inode *proc_pid_make_inode(struct super_block * sb,return NULL ; } +/* +* Generating an inode and adding it into @pid->inodes, so that task will +* invalidate inode's dentry before being released. +* +* This helper is used for creating dir-type entries under '/proc' and +* '/proc/<tgid>/task'. Other entries(eg. fd, stat) under '/proc/<tgid>' +* can be released by invalidating '/proc/<tgid>' dentry. +* In theory, dentries under '/proc/<tgid>/task' can also be released by +* invalidating '/proc/<tgid>' dentry, we reserve it to handle single +* thread exiting situation: Any one of threads should invalidate its +* '/proc/<tgid>/task/<pid>' dentry before released. +*/ +static struct inode *proc_pid_make_base_inode (struct super_block *sb, +struct task_struct *task,umode_t mode) +{ +struct inode *inode; +struct proc_inode *ei; +struct pid *pid; + + inode =proc_pid_make_inode (sb, task, mode); +if (!inode) +return NULL ; + +/* Let proc_flush_pid find this directory inode */ + ei =PROC_I (inode); + pid = ei->pid; +spin_lock (&pid->lock); +hlist_add_head_rcu (&ei->sibling_inodes, &pid->inodes); +spin_unlock (&pid->lock); + +return inode; +} +int pid_getattr (const struct path *path,struct kstat *stat,u32 request_mask,unsigned int query_flags) {
@@ -3341,7 +3369,8 @@ static struct dentry *proc_pid_instantiate(struct dentry * dentry, {struct inode *inode; - inode =proc_pid_make_inode (dentry->d_sb, task,S_IFDIR |S_IRUGO |S_IXUGO ); + inode =proc_pid_make_base_inode (dentry->d_sb, task, +S_IFDIR |S_IRUGO |S_IXUGO );if (!inode)return ERR_PTR (-ENOENT );
@@ -3637,7 +3666,8 @@ static struct dentry *proc_task_instantiate(struct dentry *dentry, struct task_struct *task, const void *ptr) { struct inode *inode; - inode =proc_pid_make_inode (dentry->d_sb, task,S_IFDIR |S_IRUGO |S_IXUGO ); + inode =proc_pid_make_base_inode (dentry->d_sb, task, +S_IFDIR |S_IRUGO |S_IXUGO );if (!inode)return ERR_PTR (-ENOENT );
Signature · 1 line
2.34.1
Greg KH
maintainerQuoted 61 lines · Suraj Jitindar Singh
From: Zhihao Cheng <[email protected]>
commit
d919a1e79bac890421537cf02ae773007bf55e6bupstream.Commit
7bc3e6e55acf06("proc: Use a list of inodes to flush from proc") movedproc_flush_task()behind__exit_signal(). Then, process systemd can take long period high cpu usage during releasing task in following concurrent processes:systemd ps
kernel_waitidstat(/proc/tgid)
do_waitfilename_lookupwait_consider_task lookup_fast release_task __exit_signal __unhash_process detach_pid __change_pid // remove task->pid_links d_revalidate -> pid_revalidate // 0 d_invalidate(/proc/tgid) shrink_dcache_parent(/proc/tgid) d_walk(/proc/tgid) spin_lock_nested(/proc/tgid/fd) // iterating opened fd proc_flush_pid | d_invalidate (/proc/tgid/fd) | shrink_dcache_parent(/proc/tgid/fd) | shrink_dentry_list(subdirs) ↓ shrink_lock_dentry(/proc/tgid/fd) --> race on dentry lockFunction
d_invalidate()will remove dentry from hash firstly, but why doesproc_flush_pid()process dentry '/proc/tgid/fd' before dentry '/proc/tgid'? That's becauseproc_pid_make_inode()adds proc inode in reverse order by invokinghlist_add_head_rcu(). But proc should not add any inodes under '/proc/tgid' except '/proc/tgid/task/pid', fix it by adding inode into 'pid->inodes' only if the inode is/proc/tgidor/proc/tgid/task/pid.Performance regression:
Create 200 tasks, each task open one file for 50,000 times. Kill all tasks when opened files exceed 10,000,000 (cat/proc/sys/fs/file-nr).Before fix:
$ time killall -wq aa real 4m40.946s # During this period, we can see 'ps' and 'systemd' taking high cpu usage.After fix:
$ time killall -wq aa real 1m20.732s # During this period, we can see 'systemd' taking high cpu usage.Fixes 7bc3e6e55acf06 ("proc: Use a list of inodes to flush from proc")Signed-off-by Zhihao Cheng <[email protected]>Signed-off-by Zhang Yi <[email protected]>Suggested-by Brian Foster <[email protected]>Reviewed-by Brian Foster <[email protected]>Cc Al Viro <[email protected]>Cc Alexey Dobriyan <[email protected]>
Quoted 8 lines
Cc Eric Biederman <[email protected]>Cc Matthew Wilcox <[email protected]>Cc Baoquan He <[email protected]>Cc Kalesh Singh <[email protected]>Cc Yu Kuai <[email protected]>Signed-off-by Andrew Morton <[email protected]>[ bp: Context adjustments ]Signed-off-by Suraj Jitindar Singh <[email protected]>
both now queued up, thanks.
greg k-h