Is it correctly that the usage for spin_{lock|unlock}_irq in clear_page_dirty_for_io
Subsystems (4)
A message that is not in the archive (sent off-list or not yet received): [email protected]
Hi, Johannes Weiner and Tejun Heo
I use linux-4.4.y to test the new cgroup controller io and the current stable kernel linux-4.4.y has the follow logic
int clear_page_dirty_for_io(struct page *page){
...
...
memcg = mem_cgroup_begin_page_stat(page); ----------(a) wb = unlocked_inode_to_wb_begin(inode, &locked); ---------(b) if (TestClearPageDirty(page)) { mem_cgroup_dec_page_stat(memcg, MEM_CGROUP_STAT_DIRTY); dec_zone_page_state(page, NR_FILE_DIRTY); dec_wb_stat(wb, WB_RECLAIMABLE);
ret =1;
}
unlocked_inode_to_wb_end(inode, locked); -----------(c) mem_cgroup_end_page_stat(memcg); -------------(d) return ret;
...
...
}
when memcg is moving, and I_WB_SWITCH flags for inode is set. the logic is the following:
spin_lock_irqsave(&memcg->move_lock, flags); -------------(a) spin_lock_irq(&inode->i_mapping->tree_lock); ------------(b) spin_unlock_irq(&inode->i_mapping->tree_lock); -----------(c) spin_unlock_irqrestore(&memcg->move_lock, flags); -----------(d)
after (c) , the local irq is enabled. I think it is not correct.
We get a deadlock backtrace after (c), the cpu get an softirq and in the irq it also call mem_cgroup_begin_page_stat to lock the same memcg->move_lock.
Since the conditions are too harsh, this scenario is difficult to reproduce. But it really exists.
So how about change (b) (c) to spin_lock_irqsave/spin_lock_irqrestore?
Thanks:-)
Quoted 38 lines · Wang Long
Hi, Johannes Weiner and Tejun Heo
I use linux-4.4.y to test the new cgroup controller io and the current stable kernel linux-4.4.y has the follow logic
int
clear_page_dirty_for_io(struct page *page){
...
...
memcg =mem_cgroup_begin_page_stat(page); ----------(a) wb =unlocked_inode_to_wb_begin(inode, &locked); ---------(b) if (TestClearPageDirty(page)) {mem_cgroup_dec_page_stat(memcg, MEM_CGROUP_STAT_DIRTY);dec_zone_page_state(page, NR_FILE_DIRTY);dec_wb_stat(wb, WB_RECLAIMABLE);
ret =1;
}
unlocked_inode_to_wb_end(inode, locked); -----------(c)mem_cgroup_end_page_stat(memcg); -------------(d) return ret;
...
...
}when memcg is moving, and
I_WB_SWITCHflags for inode is set. the logic is the following:
spin_lock_irqsave(&memcg->move_lock, flags); -------------(a)spin_lock_irq(&inode->i_mapping->tree_lock); ------------(b)spin_unlock_irq(&inode->i_mapping->tree_lock); -----------(c)spin_unlock_irqrestore(&memcg->move_lock, flags); -----------(d)after (c) , the local irq is enabled. I think it is not correct.
Quoted 8 lines
We get a deadlock backtrace after (c), the cpu get an softirq and in the irq it also call
mem_cgroup_begin_page_statto lock the samememcg->move_lock.Since the conditions are too harsh, this scenario is difficult to reproduce. But it really exists.
So how about change (b) (c) to spin_lock_irqsave/spin_lock_irqrestore?
Yes, it seems we really need this even for the current tree. Please note that At least clear_page_dirty_for_io doesn't lock memcg anymore. __cancel_dirty_page still uses lock_page_memcg though (former mem_cgroup_begin_page_stat).
Signature · 2 lines
Michal Hocko SUSE Labs
Quoted 8 lines · Michal Hocko
Quoted 7 lines · Wang Long
Hi, Johannes Weiner and Tejun Heo
I use linux-4.4.y to test the new cgroup controller io and the current stable kernel linux-4.4.y has the follow logic
Quoted 5 lines
Quoted 5 lines
int
clear_page_dirty_for_io(struct page *page){
...
...memcg = mem_cgroup_begin_page_stat(page); ----------(a) wb = unlocked_inode_to_wb_begin(inode, &locked);
---------(b)
Quoted 2 lines
Quoted 2 lines
if (TestClearPageDirty(page)) { mem_cgroup_dec_page_stat(memcg,
MEM_CGROUP_STAT_DIRTY);
Quoted 33 lines
Quoted 24 lines
dec_zone_page_state(page, NR_FILE_DIRTY); dec_wb_stat(wb, WB_RECLAIMABLE); ret =1; } unlocked_inode_to_wb_end(inode, locked); -----------(c) mem_cgroup_end_page_stat(memcg); -------------(d) return ret;...
...
}when memcg is moving, and
I_WB_SWITCHflags for inode is set. the logic is the following:
spin_lock_irqsave(&memcg->move_lock, flags); -------------(a)spin_lock_irq(&inode->i_mapping->tree_lock); ------------(b) spin_unlock_irq(&inode->i_mapping->tree_lock); -----------(c)
spin_unlock_irqrestore(&memcg->move_lock, flags); -----------(d)after (c) , the local irq is enabled. I think it is not correct.
Quoted 8 lines
We get a deadlock backtrace after (c), the cpu get an softirq and in the irq it also call
mem_cgroup_begin_page_statto lock the samememcg->move_lock.Since the conditions are too harsh, this scenario is difficult to reproduce. But it really exists.
So how about change (b) (c) to spin_lock_irqsave/spin_lock_irqrestore?
Quoted 7 lines
Yes, it seems we really need this even for the current tree. Please note that At least
clear_page_dirty_for_iodoesn't lock memcg anymore.__cancel_dirty_pagestill useslock_page_memcgthough (formermem_cgroup_begin_page_stat).
--
Michal Hocko
SUSE Labs
I agree the issue looks real in 4.4 stable and upstream. It seems like unlocked_inode_to_wb_begin/_end should use spin_lock_irqsave/restore.
I'm testing a little patch now.
Quoted 57 lines · Greg Thelen
Quoted 7 lines · Michal Hocko
Quoted 6 lines · Wang Long
Hi, Johannes Weiner and Tejun Heo
I use linux-4.4.y to test the new cgroup controller io and the current stable kernel linux-4.4.y has the follow logic
Quoted 5 lines
Quoted 5 lines
int
clear_page_dirty_for_io(struct page *page){
...
...memcg = mem_cgroup_begin_page_stat(page); ----------(a) wb = unlocked_inode_to_wb_begin(inode, &locked);---------(b)
Quoted 2 lines
Quoted 2 lines
if (TestClearPageDirty(page)) { mem_cgroup_dec_page_stat(memcg,
MEM_CGROUP_STAT_DIRTY);Quoted 31 lines
Quoted 31 lines
dec_zone_page_state(page, NR_FILE_DIRTY); dec_wb_stat(wb, WB_RECLAIMABLE); ret =1; } unlocked_inode_to_wb_end(inode, locked); -----------(c) mem_cgroup_end_page_stat(memcg); -------------(d) return ret;...
...
}when memcg is moving, and
I_WB_SWITCHflags for inode is set. the logic is the following:
spin_lock_irqsave(&memcg->move_lock, flags); -------------(a)spin_lock_irq(&inode->i_mapping->tree_lock); ------------(b) spin_unlock_irq(&inode->i_mapping->tree_lock); -----------(c)
spin_unlock_irqrestore(&memcg->move_lock, flags); -----------(d)after (c) , the local irq is enabled. I think it is not correct.
We get a deadlock backtrace after (c), the cpu get an softirq and in the irq it also call
mem_cgroup_begin_page_statto lock the samememcg->move_lock.Since the conditions are too harsh, this scenario is difficult to reproduce. But it really exists.
Quoted 8 lines
Quoted 1 line
So how about change (b) (c) to spin_lock_irqsave/spin_lock_irqrestore?
Yes, it seems we really need this even for the current tree. Please note that At least
clear_page_dirty_for_iodoesn't lock memcg anymore.__cancel_dirty_pagestill useslock_page_memcgthough (formermem_cgroup_begin_page_stat).
--
Michal Hocko
SUSE Labs
Quoted 4 lines
I agree the issue looks real in 4.4 stable and upstream. It seems like unlocked_inode_to_wb_begin/_end should use spin_lock_irqsave/restore.
I'm testing a little patch now.
Thanks.
When fix it on upstream. The longterm kernel 4.9 and 4.14 also need to fix.
PATCH 1/1 writeback: safer lock nesting
lock_page_memcg()/unlock_page_memcg() use spin_lock_irqsave/restore() if the page's memcg is undergoing move accounting, which occurs when a process leaves its memcg for a new one that has memory.move_charge_at_immigrate set.
unlocked_inode_to_wb_begin,end() use spin_lock_irq/spin_unlock_irq() if the given inode is switching writeback domains. Swithces occur when enough writes are issued from a new domain.
This existing pattern is thus suspicious:
lock_page_memcg(page);
unlocked_inode_to_wb_begin(inode, &locked);
...
unlocked_inode_to_wb_end(inode, locked);
unlock_page_memcg(page);If both inode switch and process memcg migration are both in-flight then unlocked_inode_to_wb_end() will unconditionally enable interrupts while still holding the lock_page_memcg() irq spinlock. This suggests the possibility of deadlock if an interrupt occurs before unlock_page_memcg().
truncate
__cancel_dirty_page
lock_page_memcg
unlocked_inode_to_wb_begin unlocked_inode_to_wb_end
<interrupts mistakenly enabled>
<interrupt> end_page_writeback test_clear_page_writeback lock_page_memcg <deadlock>
unlock_page_memcg
Due to configuration limitations this deadlock is not currently possible because we don't mix cgroup writeback (a cgroupv2 feature) and memory.move_charge_at_immigrate (a cgroupv1 feature).
If the kernel is hacked to always claim inode switching and memcg moving_account, then this script triggers lockup in less than a minute:
cd /mnt/cgroup/memory
mkdir a b
echo 1 > a/memory.move_charge_at_immigrate
echo 1 > b/memory.move_charge_at_immigrate
(
echo $BASHPID > a/cgroup.procs
while true; do
dd if=/dev/zero of=/mnt/big bs=1M count=256
done
) &
while true; do
sync
done &
sleep 1h &
SLEEP=$!
while true; do
echo $SLEEP > a/cgroup.procs
echo $SLEEP > b/cgroup.procs
done
Given the deadlock is not currently possible, it's debatable if there's any reason to modify the kernel. I suggest we should to prevent future surprises.
--- fs/fs-writeback.c | 5 +++-- include/linux/backing-dev.h | 18 ++++++++++++------ mm/page-writeback.c | 15 +++++++++------ 3 files changed, 24 insertions(+), 14 deletions(-)
fs/fs-writeback.c +3 -2
@@ -746,10 +746,11 @@ int inode_congested(struct inode *inode, int cong_bits)if (inode &&inode_to_wb_is_valid (inode)) {struct bdi_writeback *wb;bool locked, congested; +unsigned long flags; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &locked, &flags); congested =wb_congested (wb, cong_bits); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, locked, flags);return congested; }
include/linux/backing-dev.h +12 -6
@@ -347,6 +347,7 @@ static inline struct bdi_writeback *inode_to_wb(const struct inode *inode)* unlocked_inode_to_wb_begin - begin unlocked inode wb access transaction * @inode: target inode * @lockedp: temp bool output param, to be passed to the end function +* @flags: saved irq flags, to be passed to the end function * * The caller wants to access the wb associated with @inode but isn't * holding inode->i_lock, mapping->tree_lock or wb->list_lock. This
@@ -359,7 +360,8 @@ static inline struct bdi_writeback *inode_to_wb(const struct inode *inode)* disabled on return. */ static inline struct bdi_writeback * -unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp) +unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp, +unsigned long *flags) {rcu_read_lock ();
@@ -370,7 +372,7 @@ unlocked_inode_to_wb_begin(struct inode *inode, bool *lockedp) *lockedp =smp_load_acquire (&inode->i_state) &I_WB_SWITCH ;if (unlikely (*lockedp)) -spin_lock_irq (&inode->i_mapping->tree_lock); +spin_lock_irqsave (&inode->i_mapping->tree_lock, *flags);/* * Protected by either !I_WB_SWITCH + rcu_read_lock() or tree_lock.
@@ -383,11 +385,13 @@ unlocked_inode_to_wb_begin(struct inode *inode, bool *lockedp)* unlocked_inode_to_wb_end - end inode wb access transaction * @inode: target inode * @locked: *@lockedp from unlocked_inode_to_wb_begin() +* @flags: *@flags from unlocked_inode_to_wb_begin() */ -static inline void unlocked_inode_to_wb_end (struct inode *inode,bool locked) +static inline void unlocked_inode_to_wb_end (struct inode *inode,bool locked, +unsigned long flags) {if (unlikely (locked)) -spin_unlock_irq (&inode->i_mapping->tree_lock); +spin_unlock_irqrestore (&inode->i_mapping->tree_lock, flags);rcu_read_unlock (); }
@@ -434,12 +438,14 @@ static inline struct bdi_writeback *inode_to_wb(struct inode *inode) }static inline struct bdi_writeback * -unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp) +unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp, +unsigned long *flags) {return inode_to_wb (inode); } -static inline void unlocked_inode_to_wb_end (struct inode *inode,bool locked) +static inline void unlocked_inode_to_wb_end (struct inode *inode,bool locked, +unsigned long flags) { }
mm/page-writeback.c +9 -6
@@ -2501,13 +2501,14 @@ void account_page_redirty(struct page *page)if (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; +unsigned long flags;bool locked; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &locked, &flags); current->nr_dirtied--;dec_node_page_state (page,NR_DIRTIED );dec_wb_stat (wb,WB_DIRTIED ); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, locked, flags); } }EXPORT_SYMBOL (account_page_redirty);
@@ -2613,15 +2614,16 @@ void __cancel_dirty_page(struct page *page)if (mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; +unsigned long flags;bool locked;lock_page_memcg (page); - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &locked, &flags);if (TestClearPageDirty (page))account_page_cleaned (page, mapping, wb); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, locked, flags);unlock_page_memcg (page); }else {ClearPageDirty (page);
@@ -2653,6 +2655,7 @@ int clear_page_dirty_for_io(struct page *page)if (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; +unsigned long flags;bool locked;/*
@@ -2690,14 +2693,14 @@ int clear_page_dirty_for_io(struct page *page)* always locked coming in here, so we get the desired * exclusion. */ - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &locked, &flags);if (TestClearPageDirty (page)) {dec_lruvec_page_state (page,NR_FILE_DIRTY );dec_zone_page_state (page,NR_ZONE_WRITE_PENDING );dec_wb_stat (wb,WB_RECLAIMABLE ); ret =1 ; } -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, locked, flags);return ret; }return TestClearPageDirty (page);
Signature · 1 line
2.17.0.484.g0c8726318c-goog
Re: [PATCH] writeback: safer lock nesting
On Fri 06-04-18 01:03:24, Greg Thelen wrote:
[...]
fs/fs-writeback.c int inode_congested(struct inode *inode, int cong_bits) · line 751
Quoted 12 lines
fs/fs-writeback.c +2
-1@@ -746,10 +746,11 @@ int inode_congested(struct inode *inode, int cong_bits)if (inode &&inode_to_wb_is_valid (inode)) {struct bdi_writeback *wb;bool locked, congested; +unsigned long flags; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &locked, &flags);
Wouldn't it be better to have a cookie (struct) rather than 2 parameters and let unlocked_inode_to_wb_end DTRT?
Quoted 5 lines
fs/fs-writeback.c +1
-1congested =wb_congested (wb, cong_bits); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, locked, flags);return congested; }
Signature · 2 lines
Michal Hocko SUSE Labs
Re: [PATCH] writeback: safer lock nesting
Quoted 7 lines · Michal Hocko
On Fri 06-04-18 01:03:24, Greg Thelen wrote:
[...]fs/fs-writeback.c int inode_congested(struct inode *inode, int
Quoted 5 lines
fs/fs-writeback.c +0
-0@@ -746,10 +746,11 @@ int inode_congested(struct inode *inode, int
cong_bits)
Quoted 10 lines
fs/fs-writeback.c int inode_congested(struct inode *inode, int cong_bits) · line 751
Quoted 7 lines
fs/fs-writeback.c +2
-1if (inode &&inode_to_wb_is_valid (inode)) {struct bdi_writeback *wb;bool locked, congested; +unsigned long flags; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &locked, &flags);Wouldn't it be better to have a cookie (struct) rather than 2 parameters and let
unlocked_inode_to_wb_endDTRT?
Nod. I'll post a V2 patch with that change.
Quoted 8 lines
fs/fs-writeback.c int inode_congested(struct inode *inode, int cong_bits) · line 755
Quoted 5 lines
fs/fs-writeback.c +1
-1congested =wb_congested (wb, cong_bits); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, locked, flags);return congested; }--
Michal Hocko
SUSE Labs
PATCH v2 1/1 writeback: safer lock nesting
lock_page_memcg()/unlock_page_memcg() use spin_lock_irqsave/restore() if the page's memcg is undergoing move accounting, which occurs when a process leaves its memcg for a new one that has memory.move_charge_at_immigrate set.
unlocked_inode_to_wb_begin,end() use spin_lock_irq/spin_unlock_irq() if the given inode is switching writeback domains. Swithces occur when enough writes are issued from a new domain.
This existing pattern is thus suspicious:
lock_page_memcg(page);
unlocked_inode_to_wb_begin(inode, &locked);
...
unlocked_inode_to_wb_end(inode, locked);
unlock_page_memcg(page);If both inode switch and process memcg migration are both in-flight then unlocked_inode_to_wb_end() will unconditionally enable interrupts while still holding the lock_page_memcg() irq spinlock. This suggests the possibility of deadlock if an interrupt occurs before unlock_page_memcg().
truncate
__cancel_dirty_page
lock_page_memcg
unlocked_inode_to_wb_begin unlocked_inode_to_wb_end
<interrupts mistakenly enabled>
<interrupt> end_page_writeback test_clear_page_writeback lock_page_memcg <deadlock>
unlock_page_memcg
Due to configuration limitations this deadlock is not currently possible because we don't mix cgroup writeback (a cgroupv2 feature) and memory.move_charge_at_immigrate (a cgroupv1 feature).
If the kernel is hacked to always claim inode switching and memcg moving_account, then this script triggers lockup in less than a minute:
cd /mnt/cgroup/memory
mkdir a b
echo 1 > a/memory.move_charge_at_immigrate
echo 1 > b/memory.move_charge_at_immigrate
(
echo $BASHPID > a/cgroup.procs
while true; do
dd if=/dev/zero of=/mnt/big bs=1M count=256
done
) &
while true; do
sync
done &
sleep 1h &
SLEEP=$!
while true; do
echo $SLEEP > a/cgroup.procs
echo $SLEEP > b/cgroup.procs
done
Given the deadlock is not currently possible, it's debatable if there's any reason to modify the kernel. I suggest we should to prevent future surprises.
---
Changelog since v1:
- add wb_lock_cookie to record lock context.
fs/fs-writeback.c | 7 ++++--- include/linux/backing-dev-defs.h | 5 +++++ include/linux/backing-dev.h | 30 ++++++++++++++++-------------- mm/page-writeback.c | 18 +++++++++--------- 4 files changed, 34 insertions(+), 26 deletions(-)
fs/fs-writeback.c +4 -3
@@ -745,11 +745,12 @@ int inode_congested(struct inode *inode, int cong_bits)*/ if (inode &&inode_to_wb_is_valid (inode)) {struct bdi_writeback *wb; -bool locked, congested; +struct wb_lock_cookie lock_cookie; +bool congested; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &lock_cookie); congested =wb_congested (wb, cong_bits); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &lock_cookie);return congested; }
include/linux/backing-dev-defs.h +5 -0
@@ -223,6 +223,11 @@ static inline void set_bdi_congested(struct backing_dev_info *bdi, int sync)set_wb_congested (bdi->wb.congested, sync); } +struct wb_lock_cookie { +bool locked; +unsigned long flags; +}; +#ifdef CONFIG_CGROUP_WRITEBACK /**
include/linux/backing-dev.h +16 -14
@@ -346,7 +346,7 @@ static inline struct bdi_writeback *inode_to_wb(const struct inode *inode)/** * unlocked_inode_to_wb_begin - begin unlocked inode wb access transaction * @inode: target inode -* @lockedp: temp bool output param, to be passed to the end function +* @cookie: output param, to be passed to the end function * * The caller wants to access the wb associated with @inode but isn't * holding inode->i_lock, mapping->tree_lock or wb->list_lock. This
@@ -354,12 +354,11 @@ static inline struct bdi_writeback *inode_to_wb(const struct inode *inode)* association doesn't change until the transaction is finished with * unlocked_inode_to_wb_end(). * -* The caller must call unlocked_inode_to_wb_end() with *@lockdep -* afterwards and can't sleep during transaction. IRQ may or may not be -* disabled on return. +* The caller must call unlocked_inode_to_wb_end() with *@cookie afterwards and +* can't sleep during transaction. IRQ may or may not be disabled on return. */ static inline struct bdi_writeback * -unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp) +unlocked_inode_to_wb_begin (struct inode *inode,struct wb_lock_cookie *cookie) {rcu_read_lock ();
@@ -367,10 +366,10 @@ unlocked_inode_to_wb_begin(struct inode *inode, bool *lockedp)* Paired with store_release in inode_switch_wb_work_fn() and * ensures that we see the new wb if we see cleared I_WB_SWITCH. */ - *lockedp =smp_load_acquire (&inode->i_state) &I_WB_SWITCH ; + cookie->locked =smp_load_acquire (&inode->i_state) &I_WB_SWITCH ; -if (unlikely (*lockedp)) -spin_lock_irq (&inode->i_mapping->tree_lock); +if (unlikely (cookie->locked)) +spin_lock_irqsave (&inode->i_mapping->tree_lock, cookie->flags);/* * Protected by either !I_WB_SWITCH + rcu_read_lock() or tree_lock.
@@ -382,12 +381,14 @@ unlocked_inode_to_wb_begin(struct inode *inode, bool *lockedp)/** * unlocked_inode_to_wb_end - end inode wb access transaction * @inode: target inode -* @locked: *@lockedp from unlocked_inode_to_wb_begin() +* @cookie: @cookie from unlocked_inode_to_wb_begin() */ -static inline void unlocked_inode_to_wb_end (struct inode *inode,bool locked) +static inline void unlocked_inode_to_wb_end (struct inode *inode, +struct wb_lock_cookie *cookie) { -if (unlikely (locked)) -spin_unlock_irq (&inode->i_mapping->tree_lock); +if (unlikely (cookie->locked)) +spin_unlock_irqrestore (&inode->i_mapping->tree_lock, + cookie->flags);rcu_read_unlock (); }
@@ -434,12 +435,13 @@ static inline struct bdi_writeback *inode_to_wb(struct inode *inode) }static inline struct bdi_writeback * -unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp) +unlocked_inode_to_wb_begin (struct inode *inode,struct wb_lock_cookie *cookie) {return inode_to_wb (inode); } -static inline void unlocked_inode_to_wb_end (struct inode *inode,bool locked) +static inline void unlocked_inode_to_wb_end (struct inode *inode, +struct wb_lock_cookie *cookie) { }
mm/page-writeback.c +9 -9
@@ -2501,13 +2501,13 @@ void account_page_redirty(struct page *page)if (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie); current->nr_dirtied--;dec_node_page_state (page,NR_DIRTIED );dec_wb_stat (wb,WB_DIRTIED ); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie); } }EXPORT_SYMBOL (account_page_redirty);
@@ -2613,15 +2613,15 @@ void __cancel_dirty_page(struct page *page)if (mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie;lock_page_memcg (page); - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie);if (TestClearPageDirty (page))account_page_cleaned (page, mapping, wb); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie);unlock_page_memcg (page); }else {ClearPageDirty (page);
@@ -2653,7 +2653,7 @@ int clear_page_dirty_for_io(struct page *page)if (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie;/* * Yes, Virginia, this is indeed insane.
@@ -2690,14 +2690,14 @@ int clear_page_dirty_for_io(struct page *page)* always locked coming in here, so we get the desired * exclusion. */ - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie);if (TestClearPageDirty (page)) {dec_lruvec_page_state (page,NR_FILE_DIRTY );dec_zone_page_state (page,NR_ZONE_WRITE_PENDING );dec_wb_stat (wb,WB_RECLAIMABLE ); ret =1 ; } -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie);return ret; }return TestClearPageDirty (page);
Signature · 1 line
2.17.0.484.g0c8726318c-goog
kbuild test robot
build botRe: [PATCH v2] writeback: safer lock nesting
Thank you for the patch! Perhaps something to improve:
Hi Greg,
Thank you for the patch! Perhaps something to improve:
[auto build test WARNING on linus/master]
[also build test WARNING on v4.16]
[cannot apply to next-20180406]
[if your patch is applied to the wrong git tree, please drop us a note to help improve the system]
url: https://github.com/0day-ci/linux/commits/Greg-Thelen/writeback-safer-lock-nesting/20180407-122200
config: mips-db1xxx_defconfig (attached as .config)
compiler: mipsel-linux-gnu-gcc (Debian 7.2.0-11) 7.2.0
reproduce:
wget https://raw.githubusercontent.com/intel/lkp-tests/master/sbin/make.cross -O ~/bin/make.cross chmod +x ~/bin/make.cross
# save the attached .config to linux build tree
make.cross ARCH=mips
Note: it may well be a FALSE warning. FWIW you are at least aware of it now.
http://gcc.gnu.org/wiki/Better_Uninitialized_Warnings
All warnings (new ones prefixed by >>):
In file included from arch/mips/include/asm/atomic.h:17:0,
from include/linux/atomic.h:5,
from arch/mips/include/asm/processor.h:14, from arch/mips/include/asm/thread_info.h:16, from include/linux/thread_info.h:38, from include/asm-generic/preempt.h:5, from ./arch/mips/include/generated/asm/preempt.h:1, from include/linux/preempt.h:81,
from include/linux/spinlock.h:51,
from mm/page-writeback.c:16:
mm/page-writeback.c: In function 'account_page_redirty':
>> include/linux/irqflags.h:80:3: warning: 'cookie.flags' may be used uninitialized in this function [-Wmaybe-uninitialized]
arch_local_irq_restore(flags); \
^~~~~~~~~~~~~~~~~~~~~~
mm/page-writeback.c:2504:25: note: 'cookie.flags' was declared here
struct wb_lock_cookie cookie;
^~~~~~
In file included from arch/mips/include/asm/atomic.h:17:0,
from include/linux/atomic.h:5,
from arch/mips/include/asm/processor.h:14,
from arch/mips/include/asm/thread_info.h:16,
from include/linux/thread_info.h:38,
from include/asm-generic/preempt.h:5,
from ./arch/mips/include/generated/asm/preempt.h:1,
from include/linux/preempt.h:81,
from include/linux/spinlock.h:51,
from mm/page-writeback.c:16:
mm/page-writeback.c: In function '__cancel_dirty_page':
>> include/linux/irqflags.h:80:3: warning: 'cookie.flags' may be used uninitialized in this function [-Wmaybe-uninitialized]
arch_local_irq_restore(flags); \
^~~~~~~~~~~~~~~~~~~~~~
mm/page-writeback.c:2616:25: note: 'cookie.flags' was declared here
struct wb_lock_cookie cookie;
^~~~~~
In file included from arch/mips/include/asm/atomic.h:17:0,
from include/linux/atomic.h:5,
from arch/mips/include/asm/processor.h:14,
from arch/mips/include/asm/thread_info.h:16,
from include/linux/thread_info.h:38,
from include/asm-generic/preempt.h:5,
from ./arch/mips/include/generated/asm/preempt.h:1,
from include/linux/preempt.h:81,
from include/linux/spinlock.h:51,
from mm/page-writeback.c:16:
mm/page-writeback.c: In function 'clear_page_dirty_for_io':
>> include/linux/irqflags.h:80:3: warning: 'cookie.flags' may be used uninitialized in this function [-Wmaybe-uninitialized]
arch_local_irq_restore(flags); \
^~~~~~~~~~~~~~~~~~~~~~
mm/page-writeback.c:2656:25: note: 'cookie.flags' was declared here
struct wb_lock_cookie cookie;
^~~~~~vim +80 include/linux/irqflags.h
81d68a96 Steven Rostedt 2008-05-12 66
df9ee292 David Howells 2010-10-07 67 /*
df9ee292 David Howells 2010-10-07 68 * Wrap the arch provided IRQ routines to provide appropriate checks.
df9ee292 David Howells 2010-10-07 69 */
df9ee292 David Howells 2010-10-07 70 #define raw_local_irq_disable() arch_local_irq_disable()
df9ee292 David Howells 2010-10-07 71 #define raw_local_irq_enable() arch_local_irq_enable()
df9ee292 David Howells 2010-10-07 72 #define raw_local_irq_save(flags) \
df9ee292 David Howells 2010-10-07 73 do { \
df9ee292 David Howells 2010-10-07 74 typecheck(unsigned long, flags); \
df9ee292 David Howells 2010-10-07 75 flags = arch_local_irq_save(); \
df9ee292 David Howells 2010-10-07 76 } while (0)
df9ee292 David Howells 2010-10-07 77 #define raw_local_irq_restore(flags) \
df9ee292 David Howells 2010-10-07 78 do { \
df9ee292 David Howells 2010-10-07 79 typecheck(unsigned long, flags); \
df9ee292 David Howells 2010-10-07 @80 arch_local_irq_restore(flags); \
df9ee292 David Howells 2010-10-07 81 } while (0)
df9ee292 David Howells 2010-10-07 82 #define raw_local_save_flags(flags) \
df9ee292 David Howells 2010-10-07 83 do { \
df9ee292 David Howells 2010-10-07 84 typecheck(unsigned long, flags); \
df9ee292 David Howells 2010-10-07 85 flags = arch_local_save_flags(); \
df9ee292 David Howells 2010-10-07 86 } while (0)
df9ee292 David Howells 2010-10-07 87 #define raw_irqs_disabled_flags(flags) \
df9ee292 David Howells 2010-10-07 88 ({ \
df9ee292 David Howells 2010-10-07 89 typecheck(unsigned long, flags); \
df9ee292 David Howells 2010-10-07 90 arch_irqs_disabled_flags(flags); \
df9ee292 David Howells 2010-10-07 91 })
df9ee292 David Howells 2010-10-07 92 #define raw_irqs_disabled() (arch_irqs_disabled())
df9ee292 David Howells 2010-10-07 93 #define raw_safe_halt() arch_safe_halt()
de30a2b3 Ingo Molnar 2006-07-03 94 :::::: The code at line 80 was first introduced by commit
:::::: df9ee29270c11dba7d0fe0b83ce47a4d8e8d2101 Fix IRQ flag handling naming
:::::: TO: David Howells <[email protected]>
:::::: CC: David Howells <[email protected]>
---
0-DAY kernel test infrastructure Open Source Technology Center
https://lists.01.org/pipermail/kbuild-all Intel Corporation
PATCH v3 1/1 writeback: safer lock nesting
lock_page_memcg()/unlock_page_memcg() use spin_lock_irqsave/restore() if the page's memcg is undergoing move accounting, which occurs when a process leaves its memcg for a new one that has memory.move_charge_at_immigrate set.
unlocked_inode_to_wb_begin,end() use spin_lock_irq/spin_unlock_irq() if the given inode is switching writeback domains. Switches occur when enough writes are issued from a new domain.
This existing pattern is thus suspicious:
lock_page_memcg(page);
unlocked_inode_to_wb_begin(inode, &locked);
...
unlocked_inode_to_wb_end(inode, locked);
unlock_page_memcg(page);If both inode switch and process memcg migration are both in-flight then unlocked_inode_to_wb_end() will unconditionally enable interrupts while still holding the lock_page_memcg() irq spinlock. This suggests the possibility of deadlock if an interrupt occurs before unlock_page_memcg().
truncate
__cancel_dirty_page
lock_page_memcg
unlocked_inode_to_wb_begin unlocked_inode_to_wb_end
<interrupts mistakenly enabled>
<interrupt> end_page_writeback test_clear_page_writeback lock_page_memcg <deadlock>
unlock_page_memcg
Due to configuration limitations this deadlock is not currently possible because we don't mix cgroup writeback (a cgroupv2 feature) and memory.move_charge_at_immigrate (a cgroupv1 feature).
If the kernel is hacked to always claim inode switching and memcg moving_account, then this script triggers lockup in less than a minute:
cd /mnt/cgroup/memory
mkdir a b
echo 1 > a/memory.move_charge_at_immigrate
echo 1 > b/memory.move_charge_at_immigrate
(
echo $BASHPID > a/cgroup.procs
while true; do
dd if=/dev/zero of=/mnt/big bs=1M count=256
done
) &
while true; do
sync
done &
sleep 1h &
SLEEP=$!
while true; do
echo $SLEEP > a/cgroup.procs
echo $SLEEP > b/cgroup.procs
done
Given the deadlock is not currently possible, it's debatable if there's any reason to modify the kernel. I suggest we should to prevent future surprises.
---
Changelog since v2:
- explicitly initialize wb_lock_cookie to silence compiler warnings.
Changelog since v1:
- add wb_lock_cookie to record lock context.
fs/fs-writeback.c | 7 ++++--- include/linux/backing-dev-defs.h | 5 +++++ include/linux/backing-dev.h | 30 ++++++++++++++++-------------- mm/page-writeback.c | 18 +++++++++--------- 4 files changed, 34 insertions(+), 26 deletions(-)
fs/fs-writeback.c +4 -3
@@ -745,11 +745,12 @@ int inode_congested(struct inode *inode, int cong_bits)*/ if (inode &&inode_to_wb_is_valid (inode)) {struct bdi_writeback *wb; -bool locked, congested; +struct wb_lock_cookie lock_cookie; +bool congested; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &lock_cookie); congested =wb_congested (wb, cong_bits); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &lock_cookie);return congested; }
include/linux/backing-dev-defs.h +5 -0
@@ -223,6 +223,11 @@ static inline void set_bdi_congested(struct backing_dev_info *bdi, int sync)set_wb_congested (bdi->wb.congested, sync); } +struct wb_lock_cookie { +bool locked; +unsigned long flags; +}; +#ifdef CONFIG_CGROUP_WRITEBACK /**
include/linux/backing-dev.h +16 -14
@@ -346,7 +346,7 @@ static inline struct bdi_writeback *inode_to_wb(const struct inode *inode)/** * unlocked_inode_to_wb_begin - begin unlocked inode wb access transaction * @inode: target inode -* @lockedp: temp bool output param, to be passed to the end function +* @cookie: output param, to be passed to the end function * * The caller wants to access the wb associated with @inode but isn't * holding inode->i_lock, mapping->tree_lock or wb->list_lock. This
@@ -354,12 +354,11 @@ static inline struct bdi_writeback *inode_to_wb(const struct inode *inode)* association doesn't change until the transaction is finished with * unlocked_inode_to_wb_end(). * -* The caller must call unlocked_inode_to_wb_end() with *@lockdep -* afterwards and can't sleep during transaction. IRQ may or may not be -* disabled on return. +* The caller must call unlocked_inode_to_wb_end() with *@cookie afterwards and +* can't sleep during transaction. IRQ may or may not be disabled on return. */ static inline struct bdi_writeback * -unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp) +unlocked_inode_to_wb_begin (struct inode *inode,struct wb_lock_cookie *cookie) {rcu_read_lock ();
@@ -367,10 +366,10 @@ unlocked_inode_to_wb_begin(struct inode *inode, bool *lockedp)* Paired with store_release in inode_switch_wb_work_fn() and * ensures that we see the new wb if we see cleared I_WB_SWITCH. */ - *lockedp =smp_load_acquire (&inode->i_state) &I_WB_SWITCH ; + cookie->locked =smp_load_acquire (&inode->i_state) &I_WB_SWITCH ; -if (unlikely (*lockedp)) -spin_lock_irq (&inode->i_mapping->tree_lock); +if (unlikely (cookie->locked)) +spin_lock_irqsave (&inode->i_mapping->tree_lock, cookie->flags);/* * Protected by either !I_WB_SWITCH + rcu_read_lock() or tree_lock.
@@ -382,12 +381,14 @@ unlocked_inode_to_wb_begin(struct inode *inode, bool *lockedp)/** * unlocked_inode_to_wb_end - end inode wb access transaction * @inode: target inode -* @locked: *@lockedp from unlocked_inode_to_wb_begin() +* @cookie: @cookie from unlocked_inode_to_wb_begin() */ -static inline void unlocked_inode_to_wb_end (struct inode *inode,bool locked) +static inline void unlocked_inode_to_wb_end (struct inode *inode, +struct wb_lock_cookie *cookie) { -if (unlikely (locked)) -spin_unlock_irq (&inode->i_mapping->tree_lock); +if (unlikely (cookie->locked)) +spin_unlock_irqrestore (&inode->i_mapping->tree_lock, + cookie->flags);rcu_read_unlock (); }
@@ -434,12 +435,13 @@ static inline struct bdi_writeback *inode_to_wb(struct inode *inode) }static inline struct bdi_writeback * -unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp) +unlocked_inode_to_wb_begin (struct inode *inode,struct wb_lock_cookie *cookie) {return inode_to_wb (inode); } -static inline void unlocked_inode_to_wb_end (struct inode *inode,bool locked) +static inline void unlocked_inode_to_wb_end (struct inode *inode, +struct wb_lock_cookie *cookie) { }
mm/page-writeback.c +9 -9
@@ -2501,13 +2501,13 @@ void account_page_redirty(struct page *page)if (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie = {0 }; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie); current->nr_dirtied--;dec_node_page_state (page,NR_DIRTIED );dec_wb_stat (wb,WB_DIRTIED ); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie); } }EXPORT_SYMBOL (account_page_redirty);
@@ -2613,15 +2613,15 @@ void __cancel_dirty_page(struct page *page)if (mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie = {0 };lock_page_memcg (page); - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie);if (TestClearPageDirty (page))account_page_cleaned (page, mapping, wb); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie);unlock_page_memcg (page); }else {ClearPageDirty (page);
@@ -2653,7 +2653,7 @@ int clear_page_dirty_for_io(struct page *page)if (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie = {0 };/* * Yes, Virginia, this is indeed insane.
@@ -2690,14 +2690,14 @@ int clear_page_dirty_for_io(struct page *page)* always locked coming in here, so we get the desired * exclusion. */ - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie);if (TestClearPageDirty (page)) {dec_lruvec_page_state (page,NR_FILE_DIRTY );dec_zone_page_state (page,NR_ZONE_WRITE_PENDING );dec_wb_stat (wb,WB_RECLAIMABLE ); ret =1 ; } -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie);return ret; }return TestClearPageDirty (page);
Signature · 1 line
2.17.0.484.g0c8726318c-goog
Re: [PATCH v3] writeback: safer lock nesting
Quoted 52 lines · Greg Thelen
lock_page_memcg()/unlock_page_memcg() use spin_lock_irqsave/restore() if the page's memcg is undergoing move accounting, which occurs when a process leaves its memcg for a new one that has memory.move_charge_at_immigrate set.
unlocked_inode_to_wb_begin,end() use spin_lock_irq/spin_unlock_irq() if the given inode is switching writeback domains. Switches occur when enough writes are issued from a new domain.
This existing pattern is thus suspicious:
lock_page_memcg(page); unlocked_inode_to_wb_begin(inode, &locked); ... unlocked_inode_to_wb_end(inode, locked); unlock_page_memcg(page);If both inode switch and process memcg migration are both in-flight then
unlocked_inode_to_wb_end()will unconditionally enable interrupts while still holding thelock_page_memcg()irq spinlock. This suggests the possibility of deadlock if an interrupt occurs beforeunlock_page_memcg().truncate
__cancel_dirty_page
lock_page_memcg
unlocked_inode_to_wb_beginunlocked_inode_to_wb_end
<interrupts mistakenly enabled>
<interrupt>end_page_writebacktest_clear_page_writebacklock_page_memcg<deadlock>
unlock_page_memcgDue to configuration limitations this deadlock is not currently possible because we don't mix cgroup writeback (a cgroupv2 feature) and memory.move_charge_at_immigrate (a cgroupv1 feature).
If the kernel is hacked to always claim inode switching and memcg
moving_account, then this script triggers lockup in less than a minute:
cd/mnt/cgroup/memory
mkdir a b
echo 1 > a/memory.move_charge_at_immigrate
echo 1 > b/memory.move_charge_at_immigrate
(
echo $BASHPID > a/cgroup.procs
while true; do
dd if=/dev/zero of=/mnt/big bs=1M count=256
done
) &
while true; do
Quoted 9 lines
sync
done &
sleep 1h &
SLEEP=$!
while true; do
echo $SLEEP > a/cgroup.procs
echo $SLEEP > b/cgroup.procs
done
Very nice changelog!
Quoted 3 lines
Given the deadlock is not currently possible, it's debatable if there's any reason to modify the kernel. I suggest we should to prevent future surprises.
Agreed!
Quoted 3 lines
Reported-by Wang Long <[email protected]>Signed-off-by Greg Thelen <[email protected]>Change-Id Ibb773e8045852978f6207074491d262f1b3fb613
Not a stable material IMHO but
AFAIU
Thanks!
Quoted 195 lines
---
Changelog since v2:
- explicitly initializewb_lock_cookieto silence compiler warnings.Changelog since v1:
- addwb_lock_cookieto record lock context.fs/fs-writeback.c | 7 ++++--- include/linux/backing-dev-defs.h | 5 +++++ include/linux/backing-dev.h | 30 ++++++++++++++++-------------- mm/page-writeback.c | 18 +++++++++--------- 4 files changed, 34 insertions(+), 26 deletions(-)fs/fs-writeback.c +4
-3@@ -745,11 +745,12 @@ int inode_congested(struct inode *inode, int cong_bits)*/ if (inode &&inode_to_wb_is_valid (inode)) {struct bdi_writeback *wb; -bool locked, congested; +struct wb_lock_cookie lock_cookie; +bool congested; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &lock_cookie); congested =wb_congested (wb, cong_bits); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &lock_cookie);return congested; }include/linux/backing-dev-defs.h +5
-0@@ -223,6 +223,11 @@ static inline void set_bdi_congested(struct backing_dev_info *bdi, int sync)set_wb_congested (bdi->wb.congested, sync); } +struct wb_lock_cookie { +bool locked; +unsigned long flags; +}; +#ifdef CONFIG_CGROUP_WRITEBACK /** include/linux/backing-dev.h +16
-14@@ -346,7 +346,7 @@ static inline struct bdi_writeback *inode_to_wb(const struct inode *inode)/** * unlocked_inode_to_wb_begin - begin unlocked inode wb access transaction * @inode: target inode -* @lockedp: temp bool output param, to be passed to the end function +* @cookie: output param, to be passed to the end function * * The caller wants to access the wb associated with @inode but isn't * holding inode->i_lock, mapping->tree_lock or wb->list_lock. This @@ -354,12 +354,11 @@ static inline struct bdi_writeback *inode_to_wb(const struct inode *inode)* association doesn't change until the transaction is finished with * unlocked_inode_to_wb_end(). * -* The caller must call unlocked_inode_to_wb_end() with *@lockdep -* afterwards and can't sleep during transaction. IRQ may or may not be -* disabled on return. +* The caller must call unlocked_inode_to_wb_end() with *@cookie afterwards and +* can't sleep during transaction. IRQ may or may not be disabled on return. */ static inline struct bdi_writeback * -unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp) +unlocked_inode_to_wb_begin (struct inode *inode,struct wb_lock_cookie *cookie) {rcu_read_lock ();@@ -367,10 +366,10 @@ unlocked_inode_to_wb_begin(struct inode *inode, bool *lockedp)* Paired with store_release in inode_switch_wb_work_fn() and * ensures that we see the new wb if we see cleared I_WB_SWITCH. */ - *lockedp =smp_load_acquire (&inode->i_state) &I_WB_SWITCH ; + cookie->locked =smp_load_acquire (&inode->i_state) &I_WB_SWITCH ; -if (unlikely (*lockedp)) -spin_lock_irq (&inode->i_mapping->tree_lock); +if (unlikely (cookie->locked)) +spin_lock_irqsave (&inode->i_mapping->tree_lock, cookie->flags);/* * Protected by either !I_WB_SWITCH + rcu_read_lock() or tree_lock. @@ -382,12 +381,14 @@ unlocked_inode_to_wb_begin(struct inode *inode, bool *lockedp)/** * unlocked_inode_to_wb_end - end inode wb access transaction * @inode: target inode -* @locked: *@lockedp from unlocked_inode_to_wb_begin() +* @cookie: @cookie from unlocked_inode_to_wb_begin() */ -static inline void unlocked_inode_to_wb_end (struct inode *inode,bool locked) +static inline void unlocked_inode_to_wb_end (struct inode *inode, +struct wb_lock_cookie *cookie) { -if (unlikely (locked)) -spin_unlock_irq (&inode->i_mapping->tree_lock); +if (unlikely (cookie->locked)) +spin_unlock_irqrestore (&inode->i_mapping->tree_lock, + cookie->flags);rcu_read_unlock (); }@@ -434,12 +435,13 @@ static inline struct bdi_writeback *inode_to_wb(struct inode *inode) }static inline struct bdi_writeback * -unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp) +unlocked_inode_to_wb_begin (struct inode *inode,struct wb_lock_cookie *cookie) {return inode_to_wb (inode); } -static inline void unlocked_inode_to_wb_end (struct inode *inode,bool locked) +static inline void unlocked_inode_to_wb_end (struct inode *inode, +struct wb_lock_cookie *cookie) { }mm/page-writeback.c +9
-9@@ -2501,13 +2501,13 @@ void account_page_redirty(struct page *page)if (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie = {0 }; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie); current->nr_dirtied--;dec_node_page_state (page,NR_DIRTIED );dec_wb_stat (wb,WB_DIRTIED ); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie); } }EXPORT_SYMBOL (account_page_redirty);@@ -2613,15 +2613,15 @@ void __cancel_dirty_page(struct page *page)if (mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie = {0 };lock_page_memcg (page); - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie);if (TestClearPageDirty (page))account_page_cleaned (page, mapping, wb); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie);unlock_page_memcg (page); }else {ClearPageDirty (page);@@ -2653,7 +2653,7 @@ int clear_page_dirty_for_io(struct page *page)if (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie = {0 };/* * Yes, Virginia, this is indeed insane. @@ -2690,14 +2690,14 @@ int clear_page_dirty_for_io(struct page *page)* always locked coming in here, so we get the desired * exclusion. */ - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie);if (TestClearPageDirty (page)) {dec_lruvec_page_state (page,NR_FILE_DIRTY );dec_zone_page_state (page,NR_ZONE_WRITE_PENDING );dec_wb_stat (wb,WB_RECLAIMABLE ); ret =1 ; } -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie);return ret; }return TestClearPageDirty (page);Signature · 1 line
2.17.0.484.g0c8726318c-goog
Signature · 2 lines
Michal Hocko SUSE Labs
Andrew Morton
maintainerRe: [PATCH v3] writeback: safer lock nesting
Quoted 5 lines · On Tue, 10 Apr 2018 08:33:57 +0200 Michal Hocko <[email protected]> wrote:
Quoted 3 lines
Reported-by Wang Long <[email protected]>Signed-off-by Greg Thelen <[email protected]>Change-Id Ibb773e8045852978f6207074491d262f1b3fb613Not a stable material IMHO
Why's that? Wang Long said he's observed the deadlock three times?
Re: [PATCH v3] writeback: safer lock nesting
Quoted 9 lines · Andrew Morton
Quoted 5 lines · On Tue, 10 Apr 2018 08:33:57 +0200 Michal Hocko <[email protected]> wrote:
Quoted 3 lines
Reported-by Wang Long <[email protected]>Signed-off-by Greg Thelen <[email protected]>Change-Id Ibb773e8045852978f6207074491d262f1b3fb613Not a stable material IMHO
Why's that? Wang Long said he's observed the deadlock three times?
I thought it is just too unlikely to hit all the conditions. My fault, I have completely missed/forgot the fact Wang Long is seeing the issue happening.
No real objection for the stable backport from me.
Signature · 2 lines
Michal Hocko SUSE Labs
Re: [PATCH v3] writeback: safer lock nesting
In reply to Greg ThelenQuoted 61 lines
lock_page_memcg()/unlock_page_memcg() use spin_lock_irqsave/restore() if the page's memcg is undergoing move accounting, which occurs when a process leaves its memcg for a new one that has memory.move_charge_at_immigrate set.
unlocked_inode_to_wb_begin,end() use spin_lock_irq/spin_unlock_irq() if the given inode is switching writeback domains. Switches occur when enough writes are issued from a new domain.
This existing pattern is thus suspicious:
lock_page_memcg(page); unlocked_inode_to_wb_begin(inode, &locked); ... unlocked_inode_to_wb_end(inode, locked); unlock_page_memcg(page);If both inode switch and process memcg migration are both in-flight then
unlocked_inode_to_wb_end()will unconditionally enable interrupts while still holding thelock_page_memcg()irq spinlock. This suggests the possibility of deadlock if an interrupt occurs beforeunlock_page_memcg().truncate
__cancel_dirty_page
lock_page_memcg
unlocked_inode_to_wb_beginunlocked_inode_to_wb_end
<interrupts mistakenly enabled>
<interrupt>end_page_writebacktest_clear_page_writebacklock_page_memcg<deadlock>
unlock_page_memcgDue to configuration limitations this deadlock is not currently possible because we don't mix cgroup writeback (a cgroupv2 feature) and memory.move_charge_at_immigrate (a cgroupv1 feature).
If the kernel is hacked to always claim inode switching and memcg
moving_account, then this script triggers lockup in less than a minute:
cd/mnt/cgroup/memory
mkdir a b
echo 1 > a/memory.move_charge_at_immigrate
echo 1 > b/memory.move_charge_at_immigrate
(
echo $BASHPID > a/cgroup.procs
while true; do
dd if=/dev/zero of=/mnt/big bs=1M count=256
done
) &
while true; do
sync
done &
sleep 1h &
SLEEP=$!
while true; do
echo $SLEEP > a/cgroup.procs
echo $SLEEP > b/cgroup.procs
done
Quoted 3 lines
Given the deadlock is not currently possible, it's debatable if there's any reason to modify the kernel. I suggest we should to prevent future surprises.
This deadlock occurs three times in our environment,
this deadlock occurs three times in our environment. It is better to cc stable kernel and backport it.
thanks
Quoted 196 lines
Reported-by Wang Long <[email protected]>Signed-off-by Greg Thelen <[email protected]>Change-Id Ibb773e8045852978f6207074491d262f1b3fb613---
Changelog since v2:
- explicitly initializewb_lock_cookieto silence compiler warnings.Changelog since v1:
- addwb_lock_cookieto record lock context.fs/fs-writeback.c | 7 ++++--- include/linux/backing-dev-defs.h | 5 +++++ include/linux/backing-dev.h | 30 ++++++++++++++++-------------- mm/page-writeback.c | 18 +++++++++--------- 4 files changed, 34 insertions(+), 26 deletions(-)fs/fs-writeback.c +4
-3@@ -745,11 +745,12 @@ int inode_congested(struct inode *inode, int cong_bits)*/ if (inode &&inode_to_wb_is_valid (inode)) {struct bdi_writeback *wb; -bool locked, congested; +struct wb_lock_cookie lock_cookie; +bool congested; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &lock_cookie); congested =wb_congested (wb, cong_bits); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &lock_cookie);return congested; }include/linux/backing-dev-defs.h +5
-0@@ -223,6 +223,11 @@ static inline void set_bdi_congested(struct backing_dev_info *bdi, int sync)set_wb_congested (bdi->wb.congested, sync); } +struct wb_lock_cookie { +bool locked; +unsigned long flags; +}; +#ifdef CONFIG_CGROUP_WRITEBACK /** include/linux/backing-dev.h +16
-14@@ -346,7 +346,7 @@ static inline struct bdi_writeback *inode_to_wb(const struct inode *inode)/** * unlocked_inode_to_wb_begin - begin unlocked inode wb access transaction * @inode: target inode -* @lockedp: temp bool output param, to be passed to the end function +* @cookie: output param, to be passed to the end function * * The caller wants to access the wb associated with @inode but isn't * holding inode->i_lock, mapping->tree_lock or wb->list_lock. This @@ -354,12 +354,11 @@ static inline struct bdi_writeback *inode_to_wb(const struct inode *inode)* association doesn't change until the transaction is finished with * unlocked_inode_to_wb_end(). * -* The caller must call unlocked_inode_to_wb_end() with *@lockdep -* afterwards and can't sleep during transaction. IRQ may or may not be -* disabled on return. +* The caller must call unlocked_inode_to_wb_end() with *@cookie afterwards and +* can't sleep during transaction. IRQ may or may not be disabled on return. */ static inline struct bdi_writeback * -unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp) +unlocked_inode_to_wb_begin (struct inode *inode,struct wb_lock_cookie *cookie) {rcu_read_lock ();@@ -367,10 +366,10 @@ unlocked_inode_to_wb_begin(struct inode *inode, bool *lockedp)* Paired with store_release in inode_switch_wb_work_fn() and * ensures that we see the new wb if we see cleared I_WB_SWITCH. */ - *lockedp =smp_load_acquire (&inode->i_state) &I_WB_SWITCH ; + cookie->locked =smp_load_acquire (&inode->i_state) &I_WB_SWITCH ; -if (unlikely (*lockedp)) -spin_lock_irq (&inode->i_mapping->tree_lock); +if (unlikely (cookie->locked)) +spin_lock_irqsave (&inode->i_mapping->tree_lock, cookie->flags);/* * Protected by either !I_WB_SWITCH + rcu_read_lock() or tree_lock. @@ -382,12 +381,14 @@ unlocked_inode_to_wb_begin(struct inode *inode, bool *lockedp)/** * unlocked_inode_to_wb_end - end inode wb access transaction * @inode: target inode -* @locked: *@lockedp from unlocked_inode_to_wb_begin() +* @cookie: @cookie from unlocked_inode_to_wb_begin() */ -static inline void unlocked_inode_to_wb_end (struct inode *inode,bool locked) +static inline void unlocked_inode_to_wb_end (struct inode *inode, +struct wb_lock_cookie *cookie) { -if (unlikely (locked)) -spin_unlock_irq (&inode->i_mapping->tree_lock); +if (unlikely (cookie->locked)) +spin_unlock_irqrestore (&inode->i_mapping->tree_lock, + cookie->flags);rcu_read_unlock (); }@@ -434,12 +435,13 @@ static inline struct bdi_writeback *inode_to_wb(struct inode *inode) }static inline struct bdi_writeback * -unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp) +unlocked_inode_to_wb_begin (struct inode *inode,struct wb_lock_cookie *cookie) {return inode_to_wb (inode); } -static inline void unlocked_inode_to_wb_end (struct inode *inode,bool locked) +static inline void unlocked_inode_to_wb_end (struct inode *inode, +struct wb_lock_cookie *cookie) { }mm/page-writeback.c +9
-9@@ -2501,13 +2501,13 @@ void account_page_redirty(struct page *page)if (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie = {0 }; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie); current->nr_dirtied--;dec_node_page_state (page,NR_DIRTIED );dec_wb_stat (wb,WB_DIRTIED ); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie); } }EXPORT_SYMBOL (account_page_redirty);@@ -2613,15 +2613,15 @@ void __cancel_dirty_page(struct page *page)if (mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie = {0 };lock_page_memcg (page); - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie);if (TestClearPageDirty (page))account_page_cleaned (page, mapping, wb); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie);unlock_page_memcg (page); }else {ClearPageDirty (page);@@ -2653,7 +2653,7 @@ int clear_page_dirty_for_io(struct page *page)if (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie = {0 };/* * Yes, Virginia, this is indeed insane. @@ -2690,14 +2690,14 @@ int clear_page_dirty_for_io(struct page *page)* always locked coming in here, so we get the desired * exclusion. */ - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie);if (TestClearPageDirty (page)) {dec_lruvec_page_state (page,NR_FILE_DIRTY );dec_zone_page_state (page,NR_ZONE_WRITE_PENDING );dec_wb_stat (wb,WB_RECLAIMABLE ); ret =1 ; } -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie);return ret; }return TestClearPageDirty (page);
Re: [PATCH v3] writeback: safer lock nesting
Quoted 6 lines · Wang Long
Quoted 6 lines
lock_page_memcg()/unlock_page_memcg() use spin_lock_irqsave/restore() if the page's memcg is undergoing move accounting, which occurs when a process leaves its memcg for a new one that has memory.move_charge_at_immigrate set.
unlocked_inode_to_wb_begin,end() use spin_lock_irq/spin_unlock_irq() if
the
Quoted 55 lines
Quoted 55 lines
given inode is switching writeback domains. Switches occur when enough writes are issued from a new domain.
This existing pattern is thus suspicious:
lock_page_memcg(page); unlocked_inode_to_wb_begin(inode, &locked); ... unlocked_inode_to_wb_end(inode, locked); unlock_page_memcg(page);If both inode switch and process memcg migration are both in-flight then
unlocked_inode_to_wb_end()will unconditionally enable interrupts while still holding thelock_page_memcg()irq spinlock. This suggests the possibility of deadlock if an interrupt occurs beforeunlock_page_memcg().truncate
__cancel_dirty_page
lock_page_memcg
unlocked_inode_to_wb_beginunlocked_inode_to_wb_end
<interrupts mistakenly enabled>
<interrupt>end_page_writebacktest_clear_page_writebacklock_page_memcg<deadlock>
unlock_page_memcgDue to configuration limitations this deadlock is not currently possible because we don't mix cgroup writeback (a cgroupv2 feature) and memory.move_charge_at_immigrate (a cgroupv1 feature).
If the kernel is hacked to always claim inode switching and memcg
moving_account, then this script triggers lockup in less than a minute:
cd/mnt/cgroup/memory
mkdir a b
echo 1 > a/memory.move_charge_at_immigrate
echo 1 > b/memory.move_charge_at_immigrate
(
echo $BASHPID > a/cgroup.procs
while true; do
dd if=/dev/zero of=/mnt/big bs=1M count=256
done
) &
while true; do
sync
done &
sleep 1h &
SLEEP=$!
while true; do
echo $SLEEP > a/cgroup.procs
echo $SLEEP > b/cgroup.procs
done
Quoted 6 lines
Quoted 3 lines
Given the deadlock is not currently possible, it's debatable if there's any reason to modify the kernel. I suggest we should to prevent future surprises.
This deadlock occurs three times in our environment,
this deadlock occurs three times in our environment. It is better to cc
stable kernel and
Quoted 1 line
backport it.
That's interesting. Are you using cgroup v1 or v2? Do you enable memory.move_charge_at_immigrate?
I assume you've been using 4.4 stable. I'll look closer at it at a 4.4 stable backport.
Andrew Morton
maintainerRe: [PATCH v3] writeback: safer lock nesting
In reply to Greg ThelenQuoted 61 lines · On Mon, 9 Apr 2018 17:59:08 -0700 Greg Thelen <[email protected]> wrote:
lock_page_memcg()/unlock_page_memcg() use spin_lock_irqsave/restore() if the page's memcg is undergoing move accounting, which occurs when a process leaves its memcg for a new one that has memory.move_charge_at_immigrate set.
unlocked_inode_to_wb_begin,end() use spin_lock_irq/spin_unlock_irq() if the given inode is switching writeback domains. Switches occur when enough writes are issued from a new domain.
This existing pattern is thus suspicious:
lock_page_memcg(page); unlocked_inode_to_wb_begin(inode, &locked); ... unlocked_inode_to_wb_end(inode, locked); unlock_page_memcg(page);If both inode switch and process memcg migration are both in-flight then
unlocked_inode_to_wb_end()will unconditionally enable interrupts while still holding thelock_page_memcg()irq spinlock. This suggests the possibility of deadlock if an interrupt occurs beforeunlock_page_memcg().truncate
__cancel_dirty_page
lock_page_memcg
unlocked_inode_to_wb_beginunlocked_inode_to_wb_end
<interrupts mistakenly enabled>
<interrupt>end_page_writebacktest_clear_page_writebacklock_page_memcg<deadlock>
unlock_page_memcgDue to configuration limitations this deadlock is not currently possible because we don't mix cgroup writeback (a cgroupv2 feature) and memory.move_charge_at_immigrate (a cgroupv1 feature).
If the kernel is hacked to always claim inode switching and memcg
moving_account, then this script triggers lockup in less than a minute:
cd/mnt/cgroup/memory
mkdir a b
echo 1 > a/memory.move_charge_at_immigrate
echo 1 > b/memory.move_charge_at_immigrate
(
echo $BASHPID > a/cgroup.procs
while true; do
dd if=/dev/zero of=/mnt/big bs=1M count=256
done
) &
while true; do
sync
done &
sleep 1h &
SLEEP=$!
while true; do
echo $SLEEP > a/cgroup.procs
echo $SLEEP > b/cgroup.procs
done
Quoted 8 lines
Given the deadlock is not currently possible, it's debatable if there's any reason to modify the kernel. I suggest we should to prevent future surprises.
...
Changelog since v2:
- explicitly initializewb_lock_cookieto silence compiler warnings.
But only in some places. What's up with that?
Quoted 15 lines
...
include/linux/backing-dev.h +1
-1@@ -346,7 +346,7 @@ static inline struct bdi_writeback *inode_to_wb(const struct inode *inode)/** * unlocked_inode_to_wb_begin - begin unlocked inode wb access transaction * @inode: target inode -* @lockedp: temp bool output param, to be passed to the end function +* @cookie: output param, to be passed to the end function * * The caller wants to access the wb associated with @inode but isn't * holding inode->i_lock, mapping->tree_lock or wb->list_lock. This
include/linux/backing-dev.h static inline struct bdi_writeback *inode_to_wb(const struct inode *inode) · line 359
Quoted 9 lines
include/linux/backing-dev.h +2
-3@@ -354,12 +354,11 @@ static inline struct bdi_writeback *inode_to_wb(const struct inode *inode)* association doesn't change until the transaction is finished with * unlocked_inode_to_wb_end(). * -* The caller must call unlocked_inode_to_wb_end() with *@lockdep -* afterwards and can't sleep during transaction. IRQ may or may not be -* disabled on return. +* The caller must call unlocked_inode_to_wb_end() with *@cookie afterwards and +* can't sleep during transaction. IRQ may or may not be disabled on return. */
Grammar is a bit awkward here,
mm/page-writeback.c void account_page_redirty(struct page *page) · line 2504
Quoted 11 lines
...
mm/page-writeback.c +1
-1@@ -2501,13 +2501,13 @@ void account_page_redirty(struct page *page)if (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie = {0 };
Trivia: it's better to use "= {}" here. That has the same effect and it doesn't assume that the first field is a scalar. And indeed, the first field is a bool so it should be {false}!
So...
include/linux/backing-dev.h renamed from include/linux/backing-dev.h~writeback-safer-lock-nesting-fix +2 -1
@@ -355,7 +355,8 @@ static inline struct bdi_writeback *inod* unlocked_inode_to_wb_end(). * * The caller must call unlocked_inode_to_wb_end() with *@cookie afterwards and -* can't sleep during transaction. IRQ may or may not be disabled on return. +* can't sleep during the transaction. IRQs may or may not be disabled on +* return. */ static inline struct bdi_writeback *unlocked_inode_to_wb_begin (struct inode *inode,struct wb_lock_cookie *cookie)
mm/page-writeback.c renamed from mm/page-writeback.c~writeback-safer-lock-nesting-fix +3 -3
@@ -2501,7 +2501,7 @@ void account_page_redirty(struct page *pif (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -struct wb_lock_cookie cookie = {0 }; +struct wb_lock_cookie cookie = {}; wb =unlocked_inode_to_wb_begin (inode, &cookie); current->nr_dirtied--;
@@ -2613,7 +2613,7 @@ void __cancel_dirty_page(struct page *paif (mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -struct wb_lock_cookie cookie = {0 }; +struct wb_lock_cookie cookie = {};lock_page_memcg (page); wb =unlocked_inode_to_wb_begin (inode, &cookie);
@@ -2653,7 +2653,7 @@ int clear_page_dirty_for_io(struct pageif (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -struct wb_lock_cookie cookie = {0 }; +struct wb_lock_cookie cookie = {};/* * Yes, Virginia, this is indeed insane.
But I wonder about the remaining uninitialized wb_lock_cookies?
Re: [PATCH v3] writeback: safer lock nesting
Quoted 8 lines · Andrew Morton
Quoted 6 lines · On Mon, 9 Apr 2018 17:59:08 -0700 Greg Thelen <[email protected]> wrote:
lock_page_memcg()/unlock_page_memcg() use spin_lock_irqsave/restore() if the page's memcg is undergoing move accounting, which occurs when a process leaves its memcg for a new one that has memory.move_charge_at_immigrate set.
unlocked_inode_to_wb_begin,end() use spin_lock_irq/spin_unlock_irq() if
the
Quoted 59 lines
Quoted 59 lines
given inode is switching writeback domains. Switches occur when enough writes are issued from a new domain.
This existing pattern is thus suspicious:
lock_page_memcg(page); unlocked_inode_to_wb_begin(inode, &locked); ... unlocked_inode_to_wb_end(inode, locked); unlock_page_memcg(page);If both inode switch and process memcg migration are both in-flight then
unlocked_inode_to_wb_end()will unconditionally enable interrupts while still holding thelock_page_memcg()irq spinlock. This suggests the possibility of deadlock if an interrupt occurs beforeunlock_page_memcg().truncate
__cancel_dirty_page
lock_page_memcg
unlocked_inode_to_wb_beginunlocked_inode_to_wb_end
<interrupts mistakenly enabled>
<interrupt>end_page_writebacktest_clear_page_writebacklock_page_memcg<deadlock>
unlock_page_memcgDue to configuration limitations this deadlock is not currently possible because we don't mix cgroup writeback (a cgroupv2 feature) and memory.move_charge_at_immigrate (a cgroupv1 feature).
If the kernel is hacked to always claim inode switching and memcg
moving_account, then this script triggers lockup in less than a minute:
cd/mnt/cgroup/memory
mkdir a b
echo 1 > a/memory.move_charge_at_immigrate
echo 1 > b/memory.move_charge_at_immigrate
(
echo $BASHPID > a/cgroup.procs
while true; do
dd if=/dev/zero of=/mnt/big bs=1M count=256
done
) &
while true; do
sync
done &
sleep 1h &
SLEEP=$!
while true; do
echo $SLEEP > a/cgroup.procs
echo $SLEEP > b/cgroup.procs
doneGiven the deadlock is not currently possible, it's debatable if there's any reason to modify the kernel. I suggest we should to prevent future surprises.
Quoted 6 lines
Quoted 4 lines
...
Changelog since v2:
- explicitly initializewb_lock_cookieto silence compiler warnings.But only in some places. What's up with that?
I annotated it in places where my compiler was complaining about. But you're right. It's better to init all 4.
Quoted 6 lines
include/linux/backing-dev.h static inline struct bdi_writeback
Quoted 6 lines
...
include/linux/backing-dev.h +0
-0@@ -346,7 +346,7 @@ static inline struct bdi_writeback
*inode_to_wb(const struct inode *inode)
Quoted 2 lines
include/linux/backing-dev.h static inline struct bdi_writeback · line 347
Quoted 2 lines
include/linux/backing-dev.h +0
-0/** * unlocked_inode_to_wb_begin - begin unlocked inode wb access
transaction
Quoted 7 lines
include/linux/backing-dev.h static inline struct bdi_writeback
Quoted 7 lines
include/linux/backing-dev.h +1
-1* @inode: target inode -* @lockedp: temp bool output param, to be passed to the end function +* @cookie: output param, to be passed to the end function * * The caller wants to access the wb associated with @inode but isn't * holding inode->i_lock, mapping->tree_lock or wb->list_lock. This @@ -354,12 +354,11 @@ static inline struct bdi_writeback
*inode_to_wb(const struct inode *inode)
Quoted 5 lines
include/linux/backing-dev.h static inline struct bdi_writeback · line 358
Quoted 5 lines
include/linux/backing-dev.h +0
-2* association doesn't change until the transaction is finished with * unlocked_inode_to_wb_end(). * -* The caller must call unlocked_inode_to_wb_end() with *@lockdep -* afterwards and can't sleep during transaction. IRQ may or may not
be
Quoted 2 lines
include/linux/backing-dev.h static inline struct bdi_writeback · line 357
Quoted 2 lines
include/linux/backing-dev.h +1
-1-* disabled on return. +* The caller must call unlocked_inode_to_wb_end() with *@cookie
afterwards and
Quoted 1 line
include/linux/backing-dev.h static inline struct bdi_writeback · line 358
Quoted 1 line
include/linux/backing-dev.h +1
-0+* can't sleep during transaction. IRQ may or may not be disabled on
return.
Quoted 7 lines
include/linux/backing-dev.h static inline struct bdi_writeback · line 359
Quoted 1 line
include/linux/backing-dev.h +0
-0*/ Grammar is a bit awkward here,
Quoted 3 lines
...
Quoted 12 lines
mm/page-writeback.c void account_page_redirty(struct page *page) · line 2504
Quoted 8 lines
mm/page-writeback.c +1
-1@@ -2501,13 +2501,13 @@ void account_page_redirty(struct page *page)if (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie = {0 };Trivia: it's better to use "= {}" here. That has the same effect and it doesn't assume that the first field is a scalar. And indeed, the first field is a bool so it should be {false}!
Nod. Thanks for the tip.
include/linux/backing-dev.h static inline struct bdi_writeback *inod · line 357
Quoted 9 lines
So...
include/linux/backing-dev.h renamed from include/linux/backing-dev.h~writeback-safer-lock-nesting-fix +0
-0@@ -355,7 +355,8 @@ static inline struct bdi_writeback *inod* unlocked_inode_to_wb_end(). * * The caller must call unlocked_inode_to_wb_end() with *@cookie
afterwards and
include/linux/backing-dev.h static inline struct bdi_writeback *inod · line 358
Quoted 1 line
include/linux/backing-dev.h renamed from include/linux/backing-dev.h~writeback-safer-lock-nesting-fix +0
-1-* can't sleep during transaction. IRQ may or may not be disabled on
return.
include/linux/backing-dev.h static inline struct bdi_writeback *inod · line 358
Quoted 1 line
include/linux/backing-dev.h renamed from include/linux/backing-dev.h~writeback-safer-lock-nesting-fix +1
-0+* can't sleep during the transaction. IRQs may or may not be disabled
on
include/linux/backing-dev.h static inline struct bdi_writeback *inod
Quoted 4 lines
include/linux/backing-dev.h renamed from include/linux/backing-dev.h~writeback-safer-lock-nesting-fix +1
-0+* return. */ static inline struct bdi_writeback * unlocked_inode_to_wb_begin(struct inode *inode, struct wb_lock_cookie
*cookie)
Quoted 30 lines
mm/page-writeback.c renamed from mm/page-writeback.c~writeback-safer-lock-nesting-fix +3
-3@@ -2501,7 +2501,7 @@ void account_page_redirty(struct page *pif (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -struct wb_lock_cookie cookie = {0 }; +struct wb_lock_cookie cookie = {}; wb =unlocked_inode_to_wb_begin (inode, &cookie); current->nr_dirtied--;@@ -2613,7 +2613,7 @@ void __cancel_dirty_page(struct page *paif (mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -struct wb_lock_cookie cookie = {0 }; +struct wb_lock_cookie cookie = {};lock_page_memcg (page); wb =unlocked_inode_to_wb_begin (inode, &cookie);@@ -2653,7 +2653,7 @@ int clear_page_dirty_for_io(struct pageif (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -struct wb_lock_cookie cookie = {0 }; +struct wb_lock_cookie cookie = {};/* * Yes, Virginia, this is indeed insane.
Quoted 1 line
But I wonder about the remaining uninitialized
wb_lock_cookies?
Yeah, I'll post an v4 with everything folded in.
PATCH v4 1/1 writeback: safer lock nesting
lock_page_memcg()/unlock_page_memcg() use spin_lock_irqsave/restore() if the page's memcg is undergoing move accounting, which occurs when a process leaves its memcg for a new one that has memory.move_charge_at_immigrate set.
unlocked_inode_to_wb_begin,end() use spin_lock_irq/spin_unlock_irq() if the given inode is switching writeback domains. Switches occur when enough writes are issued from a new domain.
This existing pattern is thus suspicious:
lock_page_memcg(page);
unlocked_inode_to_wb_begin(inode, &locked);
...
unlocked_inode_to_wb_end(inode, locked);
unlock_page_memcg(page);If both inode switch and process memcg migration are both in-flight then unlocked_inode_to_wb_end() will unconditionally enable interrupts while still holding the lock_page_memcg() irq spinlock. This suggests the possibility of deadlock if an interrupt occurs before unlock_page_memcg().
truncate
__cancel_dirty_page
lock_page_memcg
unlocked_inode_to_wb_begin unlocked_inode_to_wb_end
<interrupts mistakenly enabled>
<interrupt> end_page_writeback test_clear_page_writeback lock_page_memcg <deadlock>
unlock_page_memcg
Due to configuration limitations this deadlock is not currently possible because we don't mix cgroup writeback (a cgroupv2 feature) and memory.move_charge_at_immigrate (a cgroupv1 feature).
If the kernel is hacked to always claim inode switching and memcg moving_account, then this script triggers lockup in less than a minute:
cd /mnt/cgroup/memory
mkdir a b
echo 1 > a/memory.move_charge_at_immigrate
echo 1 > b/memory.move_charge_at_immigrate
(
echo $BASHPID > a/cgroup.procs
while true; do
dd if=/dev/zero of=/mnt/big bs=1M count=256
done
) &
while true; do
sync
done &
sleep 1h &
SLEEP=$!
while true; do
echo $SLEEP > a/cgroup.procs
echo $SLEEP > b/cgroup.procs
done
The deadlock does not seem possible, so it's debatable if there's any reason to modify the kernel. I suggest we should to prevent future surprises. And Wang Long said "this deadlock occurs three times in our environment", so there's more reason to apply this, even to stable. Stable 4.4 has minor conflicts applying this patch.
For a clean 4.4 patch see "[PATCH for-4.4] writeback: safer lock nesting"
https://lkml.org/lkml/2018/4/11/146
---
Changelog since v3:
- initialize wb_lock_cookie wiht {} rather than {0}.
- comment grammar fix
- commit log footer cleanup (-Change-Id, +Fixes, +Acks, +stable), though patch does not cleanly apply to 4.4. I'll post a 4.4-stable specific patch.
Changelog since v2:
- explicitly initialize wb_lock_cookie to silence compiler warnings.
Changelog since v1:
- add wb_lock_cookie to record lock context.
fs/fs-writeback.c | 7 ++++--- include/linux/backing-dev-defs.h | 5 +++++ include/linux/backing-dev.h | 31 +++++++++++++++++-------------- mm/page-writeback.c | 18 +++++++++--------- 4 files changed, 35 insertions(+), 26 deletions(-)
fs/fs-writeback.c +4 -3
@@ -745,11 +745,12 @@ int inode_congested(struct inode *inode, int cong_bits)*/ if (inode &&inode_to_wb_is_valid (inode)) {struct bdi_writeback *wb; -bool locked, congested; +struct wb_lock_cookie lock_cookie = {}; +bool congested; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &lock_cookie); congested =wb_congested (wb, cong_bits); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &lock_cookie);return congested; }
include/linux/backing-dev-defs.h +5 -0
@@ -223,6 +223,11 @@ static inline void set_bdi_congested(struct backing_dev_info *bdi, int sync)set_wb_congested (bdi->wb.congested, sync); } +struct wb_lock_cookie { +bool locked; +unsigned long flags; +}; +#ifdef CONFIG_CGROUP_WRITEBACK /**
include/linux/backing-dev.h +17 -14
@@ -346,7 +346,7 @@ static inline struct bdi_writeback *inode_to_wb(const struct inode *inode)/** * unlocked_inode_to_wb_begin - begin unlocked inode wb access transaction * @inode: target inode -* @lockedp: temp bool output param, to be passed to the end function +* @cookie: output param, to be passed to the end function * * The caller wants to access the wb associated with @inode but isn't * holding inode->i_lock, mapping->tree_lock or wb->list_lock. This
@@ -354,12 +354,12 @@ static inline struct bdi_writeback *inode_to_wb(const struct inode *inode)* association doesn't change until the transaction is finished with * unlocked_inode_to_wb_end(). * -* The caller must call unlocked_inode_to_wb_end() with *@lockdep -* afterwards and can't sleep during transaction. IRQ may or may not be -* disabled on return. +* The caller must call unlocked_inode_to_wb_end() with *@cookie afterwards and +* can't sleep during the transaction. IRQs may or may not be disabled on +* return. */ static inline struct bdi_writeback * -unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp) +unlocked_inode_to_wb_begin (struct inode *inode,struct wb_lock_cookie *cookie) {rcu_read_lock ();
@@ -367,10 +367,10 @@ unlocked_inode_to_wb_begin(struct inode *inode, bool *lockedp)* Paired with store_release in inode_switch_wb_work_fn() and * ensures that we see the new wb if we see cleared I_WB_SWITCH. */ - *lockedp =smp_load_acquire (&inode->i_state) &I_WB_SWITCH ; + cookie->locked =smp_load_acquire (&inode->i_state) &I_WB_SWITCH ; -if (unlikely (*lockedp)) -spin_lock_irq (&inode->i_mapping->tree_lock); +if (unlikely (cookie->locked)) +spin_lock_irqsave (&inode->i_mapping->tree_lock, cookie->flags);/* * Protected by either !I_WB_SWITCH + rcu_read_lock() or tree_lock.
@@ -382,12 +382,14 @@ unlocked_inode_to_wb_begin(struct inode *inode, bool *lockedp)/** * unlocked_inode_to_wb_end - end inode wb access transaction * @inode: target inode -* @locked: *@lockedp from unlocked_inode_to_wb_begin() +* @cookie: @cookie from unlocked_inode_to_wb_begin() */ -static inline void unlocked_inode_to_wb_end (struct inode *inode,bool locked) +static inline void unlocked_inode_to_wb_end (struct inode *inode, +struct wb_lock_cookie *cookie) { -if (unlikely (locked)) -spin_unlock_irq (&inode->i_mapping->tree_lock); +if (unlikely (cookie->locked)) +spin_unlock_irqrestore (&inode->i_mapping->tree_lock, + cookie->flags);rcu_read_unlock (); }
@@ -434,12 +436,13 @@ static inline struct bdi_writeback *inode_to_wb(struct inode *inode) }static inline struct bdi_writeback * -unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp) +unlocked_inode_to_wb_begin (struct inode *inode,struct wb_lock_cookie *cookie) {return inode_to_wb (inode); } -static inline void unlocked_inode_to_wb_end (struct inode *inode,bool locked) +static inline void unlocked_inode_to_wb_end (struct inode *inode, +struct wb_lock_cookie *cookie) { }
mm/page-writeback.c +9 -9
@@ -2501,13 +2501,13 @@ void account_page_redirty(struct page *page)if (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie = {}; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie); current->nr_dirtied--;dec_node_page_state (page,NR_DIRTIED );dec_wb_stat (wb,WB_DIRTIED ); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie); } }EXPORT_SYMBOL (account_page_redirty);
@@ -2613,15 +2613,15 @@ void __cancel_dirty_page(struct page *page)if (mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie = {};lock_page_memcg (page); - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie);if (TestClearPageDirty (page))account_page_cleaned (page, mapping, wb); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie);unlock_page_memcg (page); }else {ClearPageDirty (page);
@@ -2653,7 +2653,7 @@ int clear_page_dirty_for_io(struct page *page)if (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie = {};/* * Yes, Virginia, this is indeed insane.
@@ -2690,14 +2690,14 @@ int clear_page_dirty_for_io(struct page *page)* always locked coming in here, so we get the desired * exclusion. */ - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie);if (TestClearPageDirty (page)) {dec_lruvec_page_state (page,NR_FILE_DIRTY );dec_zone_page_state (page,NR_ZONE_WRITE_PENDING );dec_wb_stat (wb,WB_RECLAIMABLE ); ret =1 ; } -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie);return ret; }return TestClearPageDirty (page);
Signature · 1 line
2.17.0.484.g0c8726318c-goog
Re: [PATCH] writeback: safer lock nesting
In reply to Greg ThelenHi,
[This is an automated email]
This commit has been processed by the -stable helper bot and determined to be a high probability candidate for -stable trees. (score: 44.5575)
The bot has tested the following trees: v4.16.1, v4.15.16, v4.14.33, v4.9.93, v4.4.127.
v4.16.1: Build OK!
v4.15.16: Build OK!
v4.14.33: Build OK!
v4.9.93: Build OK!
v4.4.127: Failed to apply! Possible dependencies:
62cccb8c8e7a ("mm: simplify lock_page_memcg()")Please let us know if you'd like to have this patch included in a stable tree.
--
Thanks,
Sasha
Re: [PATCH] writeback: safer lock nesting
Quoted 8 lines
Hi,
[This is an automated email]
This commit has been processed by the -stable helper bot and determined to be a high probability candidate for -stable trees. (score: 44.5575)
Quoted 8 lines
The bot has tested the following trees: v4.16.1, v4.15.16, v4.14.33, v4.9.93, v4.4.127.
v4.16.1: Build OK!
v4.15.16: Build OK!
v4.14.33: Build OK!
v4.9.93: Build OK!
v4.4.127: Failed to apply! Possible dependencies:62cccb8c8e7a ("mm: simplify lock_page_memcg()")
Hi Sasha,
I test the memory cgroup in lts v4.4, for this issue, 62cccb8c8e7a ("mm: simplify lock_page_memcg()") need to adjust and there are several other places that need to be fixed.
I will make the patch for lts v4.4 if no one did.
Thanks.
Quoted 7 lines
Please let us know if you'd like to have this patch included in a stable tree.
--
Thanks,
Sasha
Re: [PATCH] writeback: safer lock nesting
Quoted 8 lines · Wang Long
Quoted 8 lines
Hi,
[This is an automated email]
This commit has been processed by the -stable helper bot and determined to be a high probability candidate for -stable trees. (score: 44.5575)
The bot has tested the following trees: v4.16.1, v4.15.16, v4.14.33,
v4.9.93, v4.4.127.
Quoted 7 lines
Quoted 7 lines
v4.16.1: Build OK!
v4.15.16: Build OK!
v4.14.33: Build OK!
v4.9.93: Build OK!
v4.4.127: Failed to apply! Possible dependencies:62cccb8c8e7a ("mm: simplify lock_page_memcg()")
Quoted 7 lines
Hi Sasha,
I test the memory cgroup in lts v4.4, for this issue,
62cccb8c8e7a("mm:
simplifylock_page_memcg()")
need to adjust and there are several other places that need to be fixed.I will make the patch for lts v4.4 if no one did.
I'm testing a 4.4-stable patch right now. ETA is a few hours.
PATCH 1/1 writeback: safer lock nesting
lock_page_memcg()/unlock_page_memcg() use spin_lock_irqsave/restore() if the page's memcg is undergoing move accounting, which occurs when a process leaves its memcg for a new one that has memory.move_charge_at_immigrate set.
unlocked_inode_to_wb_begin,end() use spin_lock_irq/spin_unlock_irq() if the given inode is switching writeback domains. Switches occur when enough writes are issued from a new domain.
This existing pattern is thus suspicious:
lock_page_memcg(page);
unlocked_inode_to_wb_begin(inode, &locked);
...
unlocked_inode_to_wb_end(inode, locked);
unlock_page_memcg(page);If both inode switch and process memcg migration are both in-flight then unlocked_inode_to_wb_end() will unconditionally enable interrupts while still holding the lock_page_memcg() irq spinlock. This suggests the possibility of deadlock if an interrupt occurs before unlock_page_memcg().
truncate
__cancel_dirty_page
lock_page_memcg
unlocked_inode_to_wb_begin unlocked_inode_to_wb_end
<interrupts mistakenly enabled>
<interrupt> end_page_writeback test_clear_page_writeback lock_page_memcg <deadlock>
unlock_page_memcg
Due to configuration limitations this deadlock is not currently possible because we don't mix cgroup writeback (a cgroupv2 feature) and memory.move_charge_at_immigrate (a cgroupv1 feature).
If the kernel is hacked to always claim inode switching and memcg moving_account, then this script triggers lockup in less than a minute:
cd /mnt/cgroup/memory
mkdir a b
echo 1 > a/memory.move_charge_at_immigrate
echo 1 > b/memory.move_charge_at_immigrate
(
echo $BASHPID > a/cgroup.procs
while true; do
dd if=/dev/zero of=/mnt/big bs=1M count=256
done
) &
while true; do
sync
done &
sleep 1h &
SLEEP=$!
while true; do
echo $SLEEP > a/cgroup.procs
echo $SLEEP > b/cgroup.procs
done
The deadlock does not seem possible, so it's debatable if there's any reason to modify the kernel. I suggest we should to prevent future surprises. And Wang Long said "this deadlock occurs three times in our environment", so there's more reason to apply this, even to stable.
[ This patch is only for 4.4 stable. Newer stable kernels should use be able to
cherry pick the upstream "writeback: safer lock nesting" patch. ]
--- fs/fs-writeback.c | 7 ++++--- include/linux/backing-dev-defs.h | 5 +++++ include/linux/backing-dev.h | 31 +++++++++++++++++-------------- mm/page-writeback.c | 18 +++++++++--------- 4 files changed, 35 insertions(+), 26 deletions(-)
fs/fs-writeback.c +4 -3
@@ -747,11 +747,12 @@ int inode_congested(struct inode *inode, int cong_bits)*/ if (inode &&inode_to_wb_is_valid (inode)) {struct bdi_writeback *wb; -bool locked, congested; +struct wb_lock_cookie lock_cookie = {}; +bool congested; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &lock_cookie); congested =wb_congested (wb, cong_bits); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &lock_cookie);return congested; }
include/linux/backing-dev-defs.h +5 -0
@@ -191,6 +191,11 @@ static inline void set_bdi_congested(struct backing_dev_info *bdi, int sync)set_wb_congested (bdi->wb.congested, sync); } +struct wb_lock_cookie { +bool locked; +unsigned long flags; +}; +#ifdef CONFIG_CGROUP_WRITEBACK /**
include/linux/backing-dev.h +17 -14
@@ -366,7 +366,7 @@ static inline struct bdi_writeback *inode_to_wb(struct inode *inode)/** * unlocked_inode_to_wb_begin - begin unlocked inode wb access transaction * @inode: target inode -* @lockedp: temp bool output param, to be passed to the end function +* @cookie: output param, to be passed to the end function * * The caller wants to access the wb associated with @inode but isn't * holding inode->i_lock, mapping->tree_lock or wb->list_lock. This
@@ -374,12 +374,12 @@ static inline struct bdi_writeback *inode_to_wb(struct inode *inode)* association doesn't change until the transaction is finished with * unlocked_inode_to_wb_end(). * -* The caller must call unlocked_inode_to_wb_end() with *@lockdep -* afterwards and can't sleep during transaction. IRQ may or may not be -* disabled on return. +* The caller must call unlocked_inode_to_wb_end() with *@cookie afterwards and +* can't sleep during the transaction. IRQs may or may not be disabled on +* return. */ static inline struct bdi_writeback * -unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp) +unlocked_inode_to_wb_begin (struct inode *inode,struct wb_lock_cookie *cookie) {rcu_read_lock ();
@@ -387,10 +387,10 @@ unlocked_inode_to_wb_begin(struct inode *inode, bool *lockedp)* Paired with store_release in inode_switch_wb_work_fn() and * ensures that we see the new wb if we see cleared I_WB_SWITCH. */ - *lockedp =smp_load_acquire (&inode->i_state) &I_WB_SWITCH ; + cookie->locked =smp_load_acquire (&inode->i_state) &I_WB_SWITCH ; -if (unlikely (*lockedp)) -spin_lock_irq (&inode->i_mapping->tree_lock); +if (unlikely (cookie->locked)) +spin_lock_irqsave (&inode->i_mapping->tree_lock, cookie->flags);/* * Protected by either !I_WB_SWITCH + rcu_read_lock() or tree_lock.
@@ -402,12 +402,14 @@ unlocked_inode_to_wb_begin(struct inode *inode, bool *lockedp)/** * unlocked_inode_to_wb_end - end inode wb access transaction * @inode: target inode -* @locked: *@lockedp from unlocked_inode_to_wb_begin() +* @cookie: @cookie from unlocked_inode_to_wb_begin() */ -static inline void unlocked_inode_to_wb_end (struct inode *inode,bool locked) +static inline void unlocked_inode_to_wb_end (struct inode *inode, +struct wb_lock_cookie *cookie) { -if (unlikely (locked)) -spin_unlock_irq (&inode->i_mapping->tree_lock); +if (unlikely (cookie->locked)) +spin_unlock_irqrestore (&inode->i_mapping->tree_lock, + cookie->flags);rcu_read_unlock (); }
@@ -454,12 +456,13 @@ static inline struct bdi_writeback *inode_to_wb(struct inode *inode) }static inline struct bdi_writeback * -unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp) +unlocked_inode_to_wb_begin (struct inode *inode,struct wb_lock_cookie *cookie) {return inode_to_wb (inode); } -static inline void unlocked_inode_to_wb_end (struct inode *inode,bool locked) +static inline void unlocked_inode_to_wb_end (struct inode *inode, +struct wb_lock_cookie *cookie) { }
mm/page-writeback.c +9 -9
@@ -2510,13 +2510,13 @@ void account_page_redirty(struct page *page)if (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie = {}; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie); current->nr_dirtied--;dec_zone_page_state (page,NR_DIRTIED );dec_wb_stat (wb,WB_DIRTIED ); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie); } }EXPORT_SYMBOL (account_page_redirty);
@@ -2622,15 +2622,15 @@ void cancel_dirty_page(struct page *page)struct inode *inode = mapping->host;struct bdi_writeback *wb;struct mem_cgroup *memcg; -bool locked; +struct wb_lock_cookie cookie = {}; memcg =mem_cgroup_begin_page_stat (page); - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie);if (TestClearPageDirty (page))account_page_cleaned (page, mapping, memcg, wb); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie);mem_cgroup_end_page_stat (memcg); }else {ClearPageDirty (page);
@@ -2663,7 +2663,7 @@ int clear_page_dirty_for_io(struct page *page)struct inode *inode = mapping->host;struct bdi_writeback *wb;struct mem_cgroup *memcg; -bool locked; +struct wb_lock_cookie cookie = {};/* * Yes, Virginia, this is indeed insane.
@@ -2701,14 +2701,14 @@ int clear_page_dirty_for_io(struct page *page)* exclusion. */ memcg =mem_cgroup_begin_page_stat (page); - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie);if (TestClearPageDirty (page)) {mem_cgroup_dec_page_stat (memcg,MEM_CGROUP_STAT_DIRTY );dec_zone_page_state (page,NR_FILE_DIRTY );dec_wb_stat (wb,WB_RECLAIMABLE ); ret =1 ; } -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie);mem_cgroup_end_page_stat (memcg);return ret; }
Signature · 1 line
2.17.0.484.g0c8726318c-goog
Re: [PATCH for-4.4] writeback: safer lock nesting
Quoted 6 lines · Greg Thelen
lock_page_memcg()/unlock_page_memcg() use spin_lock_irqsave/restore() if the page's memcg is undergoing move accounting, which occurs when a process leaves its memcg for a new one that has memory.move_charge_at_immigrate set.
unlocked_inode_to_wb_begin,end() use spin_lock_irq/spin_unlock_irq() if
the
Quoted 55 lines
given inode is switching writeback domains. Switches occur when enough writes are issued from a new domain.
This existing pattern is thus suspicious:
lock_page_memcg(page); unlocked_inode_to_wb_begin(inode, &locked); ... unlocked_inode_to_wb_end(inode, locked); unlock_page_memcg(page);If both inode switch and process memcg migration are both in-flight then
unlocked_inode_to_wb_end()will unconditionally enable interrupts while still holding thelock_page_memcg()irq spinlock. This suggests the possibility of deadlock if an interrupt occurs beforeunlock_page_memcg().truncate
__cancel_dirty_page
lock_page_memcg
unlocked_inode_to_wb_beginunlocked_inode_to_wb_end
<interrupts mistakenly enabled>
<interrupt>end_page_writebacktest_clear_page_writebacklock_page_memcg<deadlock>
unlock_page_memcgDue to configuration limitations this deadlock is not currently possible because we don't mix cgroup writeback (a cgroupv2 feature) and memory.move_charge_at_immigrate (a cgroupv1 feature).
If the kernel is hacked to always claim inode switching and memcg
moving_account, then this script triggers lockup in less than a minute:
cd/mnt/cgroup/memory
mkdir a b
echo 1 > a/memory.move_charge_at_immigrate
echo 1 > b/memory.move_charge_at_immigrate
(
echo $BASHPID > a/cgroup.procs
while true; do
dd if=/dev/zero of=/mnt/big bs=1M count=256
done
) &
while true; do
sync
done &
sleep 1h &
SLEEP=$!
while true; do
echo $SLEEP > a/cgroup.procs
echo $SLEEP > b/cgroup.procs
done
Quoted 4 lines
The deadlock does not seem possible, so it's debatable if there's any reason to modify the kernel. I suggest we should to prevent future surprises. And Wang Long said "this deadlock occurs three times in our environment", so there's more reason to apply this, even to stable.
Wang Long: I wasn't able to reproduce the 4.4 problem. But tracing suggests this 4.4 patch is effective. If you can reproduce the problem in your 4.4 environment, then it'd be nice to confirm this fixes it. Thanks!
Quoted 1 line
[ This patch is only for 4.4 stable. Newer stable kernels should use be
able to
Quoted 3 lines
cherry pick the upstream "writeback: safer lock nesting" patch. ]
Fixes 682aa8e1a6a1 ("writeback: implement unlocked_inode_to_wb
transaction and use it for stat updates")
fs/fs-writeback.c int inode_congested(struct inode *inode, int
Quoted 17 lines
Cc [email protected] # v4.2+Reported-by Wang Long <[email protected]>Signed-off-by Greg Thelen <[email protected]>Acked-by Michal Hocko <[email protected]>Acked-by Wang Long <[email protected]>--- fs/fs-writeback.c | 7 ++++--- include/linux/backing-dev-defs.h | 5 +++++ include/linux/backing-dev.h | 31 +++++++++++++++++-------------- mm/page-writeback.c | 18 +++++++++--------- 4 files changed, 35 insertions(+), 26 deletions(-)fs/fs-writeback.c +0
-0@@ -747,11 +747,12 @@ int inode_congested(struct inode *inode, int
cong_bits)
Review
Quoted 16 lines
fs/fs-writeback.c +4
-3*/ if (inode &&inode_to_wb_is_valid (inode)) {struct bdi_writeback *wb; -bool locked, congested; +struct wb_lock_cookie lock_cookie = {}; +bool congested; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &lock_cookie); congested =wb_congested (wb, cong_bits); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &lock_cookie);return congested; }(quoted diff) +0
-0
b/include/linux/backing-dev-defs.h
include/linux/backing-dev-defs.h static inline void set_bdi_congested(struct
Quoted 4 lines
index
140c29635069..a307c37c2e6c100644include/linux/backing-dev-defs.h +0
-0@@ -191,6 +191,11 @@ static inline void set_bdi_congested(struct
backing_dev_info *bdi, int sync)
include/linux/backing-dev.h static inline struct bdi_writeback
Quoted 16 lines
include/linux/backing-dev-defs.h +5
-0set_wb_congested (bdi->wb.congested, sync); } +struct wb_lock_cookie { +bool locked; +unsigned long flags; +}; +#ifdef CONFIG_CGROUP_WRITEBACK /** include/linux/backing-dev.h +0
-0@@ -366,7 +366,7 @@ static inline struct bdi_writeback
*inode_to_wb(struct inode *inode)
include/linux/backing-dev.h static inline struct bdi_writeback · line 367
Quoted 2 lines
include/linux/backing-dev.h +0
-0/** * unlocked_inode_to_wb_begin - begin unlocked inode wb access
transaction
include/linux/backing-dev.h static inline struct bdi_writeback
Quoted 7 lines
include/linux/backing-dev.h +1
-1* @inode: target inode -* @lockedp: temp bool output param, to be passed to the end function +* @cookie: output param, to be passed to the end function * * The caller wants to access the wb associated with @inode but isn't * holding inode->i_lock, mapping->tree_lock or wb->list_lock. This @@ -374,12 +374,12 @@ static inline struct bdi_writeback
*inode_to_wb(struct inode *inode)
include/linux/backing-dev.h static inline struct bdi_writeback *inode_to_wb(struct inode *inode)
Quoted 7 lines
include/linux/backing-dev.h +1
-3* association doesn't change until the transaction is finished with * unlocked_inode_to_wb_end(). * -* The caller must call unlocked_inode_to_wb_end() with *@lockdep -* afterwards and can't sleep during transaction. IRQ may or may not be -* disabled on return. +* The caller must call unlocked_inode_to_wb_end() with *@cookie
afterwards and
include/linux/backing-dev.h static inline struct bdi_writeback · line 378
Quoted 1 line
include/linux/backing-dev.h +1
-0+* can't sleep during the transaction. IRQs may or may not be disabled
on
include/linux/backing-dev.h static inline struct bdi_writeback *inode_to_wb(struct inode *inode)
Quoted 5 lines
include/linux/backing-dev.h +2
-1+* return. */ static inline struct bdi_writeback * -unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp) +unlocked_inode_to_wb_begin(struct inode *inode, struct wb_lock_cookie
*cookie)
include/linux/backing-dev.h unlocked_inode_to_wb_begin(struct inode *inode,
Quoted 4 lines
include/linux/backing-dev.h +0
-0{rcu_read_lock ();@@ -387,10 +387,10 @@ unlocked_inode_to_wb_begin(struct inode *inode,
bool *lockedp)
include/linux/backing-dev.h unlocked_inode_to_wb_begin(struct inode *inode, bool *lockedp)
Quoted 10 lines
include/linux/backing-dev.h +3
-3* Paired with store_release in inode_switch_wb_work_fn() and * ensures that we see the new wb if we see cleared I_WB_SWITCH. */ - *lockedp =smp_load_acquire (&inode->i_state) &I_WB_SWITCH ; + cookie->locked =smp_load_acquire (&inode->i_state) &I_WB_SWITCH ; -if (unlikely (*lockedp)) -spin_lock_irq (&inode->i_mapping->tree_lock); +if (unlikely (cookie->locked)) + spin_lock_irqsave(&inode->i_mapping->tree_lock,
cookie->flags);
include/linux/backing-dev.h unlocked_inode_to_wb_begin(struct inode *inode, · line 395
Quoted 2 lines
include/linux/backing-dev.h +0
-0/* * Protected by either !I_WB_SWITCH + rcu_read_lock() or
tree_lock.
include/linux/backing-dev.h unlocked_inode_to_wb_begin(struct inode *inode,
Quoted 1 line
include/linux/backing-dev.h +0
-0@@ -402,12 +402,14 @@ unlocked_inode_to_wb_begin(struct inode *inode,
bool *lockedp)
include/linux/backing-dev.h unlocked_inode_to_wb_begin(struct inode *inode, bool *lockedp)
Quoted 7 lines
include/linux/backing-dev.h +1
-2/** * unlocked_inode_to_wb_end - end inode wb access transaction * @inode: target inode -* @locked: *@lockedp from unlocked_inode_to_wb_begin() +* @cookie: @cookie from unlocked_inode_to_wb_begin() */ -static inline void unlocked_inode_to_wb_end(struct inode *inode, bool
locked)
include/linux/backing-dev.h static inline struct bdi_writeback
Quoted 12 lines
include/linux/backing-dev.h +5
-2+static inline void unlocked_inode_to_wb_end (struct inode *inode, +struct wb_lock_cookie *cookie) { -if (unlikely (locked)) -spin_unlock_irq (&inode->i_mapping->tree_lock); +if (unlikely (cookie->locked)) +spin_unlock_irqrestore (&inode->i_mapping->tree_lock, + cookie->flags);rcu_read_unlock (); }@@ -454,12 +456,13 @@ static inline struct bdi_writeback
*inode_to_wb(struct inode *inode)
include/linux/backing-dev.h static inline struct bdi_writeback *inode_to_wb(struct inode *inode)
Quoted 5 lines
include/linux/backing-dev.h +1
-1}static inline struct bdi_writeback * -unlocked_inode_to_wb_begin (struct inode *inode,bool *lockedp) +unlocked_inode_to_wb_begin(struct inode *inode, struct wb_lock_cookie
*cookie)
include/linux/backing-dev.h static inline struct bdi_writeback *inode_to_wb(struct inode *inode)
Quoted 5 lines
include/linux/backing-dev.h +0
-1{return inode_to_wb (inode); } -static inline void unlocked_inode_to_wb_end(struct inode *inode, bool
locked)
Quoted 55 lines
include/linux/backing-dev.h +2
-0+static inline void unlocked_inode_to_wb_end (struct inode *inode, +struct wb_lock_cookie *cookie) { }mm/page-writeback.c +7
-7@@ -2510,13 +2510,13 @@ void account_page_redirty(struct page *page)if (mapping &&mapping_cap_account_dirty (mapping)) {struct inode *inode = mapping->host;struct bdi_writeback *wb; -bool locked; +struct wb_lock_cookie cookie = {}; - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie); current->nr_dirtied--;dec_zone_page_state (page,NR_DIRTIED );dec_wb_stat (wb,WB_DIRTIED ); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie); } }EXPORT_SYMBOL (account_page_redirty);@@ -2622,15 +2622,15 @@ void cancel_dirty_page(struct page *page)struct inode *inode = mapping->host;struct bdi_writeback *wb;struct mem_cgroup *memcg; -bool locked; +struct wb_lock_cookie cookie = {}; memcg =mem_cgroup_begin_page_stat (page); - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie);if (TestClearPageDirty (page))account_page_cleaned (page, mapping, memcg, wb); -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie);mem_cgroup_end_page_stat (memcg); }else {ClearPageDirty (page);@@ -2663,7 +2663,7 @@ int clear_page_dirty_for_io(struct page *page)struct inode *inode = mapping->host;struct bdi_writeback *wb;struct mem_cgroup *memcg; -bool locked; +struct wb_lock_cookie cookie = {};/* * Yes, Virginia, this is indeed insane.
mm/page-writeback.c int clear_page_dirty_for_io(struct page *page) · line 2706
Quoted 7 lines
mm/page-writeback.c +1
-1@@ -2701,14 +2701,14 @@ int clear_page_dirty_for_io(struct page *page)* exclusion. */ memcg =mem_cgroup_begin_page_stat (page); - wb =unlocked_inode_to_wb_begin (inode, &locked); + wb =unlocked_inode_to_wb_begin (inode, &cookie);if (TestClearPageDirty (page)) { mem_cgroup_dec_page_stat(memcg,
MEM_CGROUP_STAT_DIRTY);
Quoted 11 lines
mm/page-writeback.c +1
-2dec_zone_page_state (page,NR_FILE_DIRTY );dec_wb_stat (wb,WB_RECLAIMABLE ); ret =1 ; } -unlocked_inode_to_wb_end (inode, locked); +unlocked_inode_to_wb_end (inode, &cookie);mem_cgroup_end_page_stat (memcg);return ret; } --2.17.0.484.g0c8726318c-goog