2.[45] fixes for design locking bug in wait_on_page/wait_on_buffer/get_request_wait
Subsystems (5)
I recently found and fixed a misterious hang that could hang the kernel with tasks in D state with the disk idle. We could reproduce very long hangs (several hours) with tasks in D state with reiserfs after some hour of some intensive load running (not cerberus, see below why), but it wasn't a reiserfs specific problem, reiserfs just happens to take the lock_super while doing the fsync_buffer_list and this leads kupdate to get stuck in the lock_super waiting a wait_on_buffer to return, so with kupdate stuck the background run_task_queue() doesn't run every 5 seconds anymore and in turn if there's a missing unplug somewhere it will lead to an hanging machine in wait_on_buffer for indefinite/infinite time (kind of deadlock, unless somebody can trigger a readpage or something that unplugs the disk queue, usually logging in with ssh fixed the problem). Increasing singificantly the kupdate interval would potentially lead to the same indefinite hang on a ext2 while running fsync.
For some time I didn't even consider the possibility of wait_on_buffer being the problem, there are over 700 patches applied in the kernel where we could reproduce so for some time I was looking at everything but the buggy place. After ruling out various other bits (scheduler fixes/compiler/fsync corruption fixes etc..) I actually realized the problem is a longstanding design locking problem in wait_on_buffer (then I found the same problem in wait_on_page and yesterday Chris found a similar problem in get_request_wait too, the get_request_wait is not exactly the same issue, but it's quite similar and it could lead to exactly the same hangs).
Probably nobody noticed this yet because normally with ext2/ext3 these hangs happens in all the machines but they are resolved after a disk idle time of 2.5 seconds in mean and they happens once in a while, normally people would see mean delays of 2.5 sec caming from the datacenter and they would think it's a normal I/O congestion or the elevator or something during the fsync on ext2. Furthmore as Chris pointed out with very intensive load bdflush would be usually running in the background, this race can trigger only with mid writepage loads when bdflush/pdflush has no reason to run.
We also have the lowlatency fixes (they're fixes) inside submit_bh so we probably opened a larger window for the race to trigger than mainline. Chris also double checked the bug we were facing was really this race by introducing a delay in submit_bh to make it reproducible in a reasonable amount of time.
the race looks like this:
CPU0 CPU1
----------------- ------------------------
reiserfs_writepage
lock_buffer()
fsync_buffers_list() under lock_super()
wait_on_buffer()
run_task_queue(&tq_disk) -> noop
schedule() <- hang with lock_super acquired
submit_bh()
/* don't unplug here */ This example is reiserfs specific but any wait_on_buffer can definitely hang indefinitely against any concurrent ll_rw_block or submit_bh (even on UP since submit_bh is a blocking operation and in particular with the lowlat fixes). There's no big kernel lock anymore serializing wait_on_buffer/ll_rw_block. This design locking problem was introduced with the removal of the BKL from wait_on_buffer/wait_on_page/ll_rw_blk during one of the 2.3 scalability efforts. So any 2.4 kernel out there is affected by this race.
in short the problem here is that the wait_on_"something" has no clue if the locked "something" is just inserted in the I/O queue and visible to the device, so it has no clue if the run_task_queue may become a noop or if it may affect the "something". And the writer side that executes the submit_bh won't unplug the queue rightfully (to allow merging and boost performance until somebody actually asks for the I/O completed ASAP).
I fixed the race by simply doing a wakeup of any waiter after any submit_bh/submit_bio that left stuff pending in the I/O queue. So if the race triggers now the wait_on_something will get a wakeup and in turn it will trigger the unplug again closing the window for the race. This is fixing the problem in practice and it seems the best fix at least for 2.4, and I don't see any potential performance regression, so I don't feel the need of anything more complicated than this, the race triggers once every several hours only under some special workload. You may try to avoid loading the waitqueue head cacheline during submit_bh, but at least for 2.4 I don't think it worth the complexity and it's an I/O path anyways so it's certainly not critical.
The problem noticed by Chris with get_request_wait is similar, the unplugging was run before adding the task to the waitqueue, so the unplug could free the requests and somebody else could allocate the freed requests without unplugging the queue afterwards. I fixed it simply by unplugging the queue just before schedule(). That was really a more genuine bug than the other subtle ones. With get_request_wait the fix is so simple because we deal with entities that are guaranteed to be affected by the queue unplug always (they're the I/O requests), this isn't the case with the locked bh or locked/writeback pages, that was infact the wrong assumption that allowed the other races to trigger in the first place.
while doing these fixes I noticed various other bits:
1) in general the blk_run_queues()/run_task_queue()/sync_page should always run just before schedule(), it's pointless to unplug anything if we don't run schedule (ultramicrooptimization)
2) in 2.4 the run_task_queue() in wait_on_buffer could have its TQ_ACTIVE executed inside the add_wait_queue critical section since spin_unlock has inclusive semantics (literally speculative reads can pass the spin_unlock even on x86/x86-64)
3) the __blk_put_request/blkdev_release_request was increasing the count and reading the waitqueue contents without even a barrier() for the asm layer, it needs an smp_mb() in between to serialize against get_request_wait that runs locklessy
I did two patches one for 2.4.20rc1 and one for 2.5.47 (sorry no bk tree here, I will try to make bitdropper.py available shortly so I can access the new info encoded in proprietary form too) that will address all these races. However 2.5 should be analysed further, I didn't search too hard for all the possible places that could have this race in 2.5, I searched hard in 2.4 and I only addressed all the same problems in 2.5. The only bit I think could be problematic in 2.4 is the nfs specualtive I/O, the reason nfs is implementing a sync_page in the first place. That may have the same race, I heard infact of some report with nfs hung in wait_on_page, and I wonder if this could explain it too. I assume the fs maintainers will take care of checking their fs for missing wakeups of page waiters in 2.4 and 2.5 now that the problem is well known.
http://www.us.kernel.org/pub/linux/kernel/people/andrea/patches/v2.4/2.4.20rc1/fix-pausing-1 http://www.us.kernel.org/pub/linux/kernel/people/andrea/patches/v2.5/2.5.47/fix-pausing-1
they're attached to this email too since they're tiny.
Andrea
drivers/block/ll_rw_blk.c +20 -5
@@ -590,12 +590,20 @@ static struct request *__get_request_wai registerstruct request *rq;DECLARE_WAITQUEUE (wait, current); -generic_unplug_device (q);add_wait_queue_exclusive (&q->wait_for_requests[rw], &wait); do { set_current_state(TASK_UNINTERRUPTIBLE); - if (q->rq[rw].count == 0) + if (q->rq[rw].count == 0) { + /* + * All we care about is not to stall if any request + * is been released after we set TASK_UNINTERRUPTIBLE. + * This is the most efficient place to unplug the queue + * in case we hit the race and we can get the request + * without waiting. + */ + generic_unplug_device(q); schedule(); + } spin_lock_irq(&io_request_lock); rq = get_request(q, rw); spin_unlock_irq(&io_request_lock);
@@ -829,9 +837,11 @@ void blkdev_release_request(struct reque*/ if (q) {list_add (&req->queue, &q->rq[rw].free); -if (++q->rq[rw].count >= q->batch_requests && -waitqueue_active (&q->wait_for_requests[rw])) -wake_up (&q->wait_for_requests[rw]); +if (++q->rq[rw].count >= q->batch_requests) { +smp_mb (); +if (waitqueue_active (&q->wait_for_requests[rw])) +wake_up (&q->wait_for_requests[rw]); + } } }
@@ -1200,6 +1210,11 @@ void submit_bh(int rw, struct buffer_heageneric_make_request (rw, bh); +/* fix race condition with wait_on_buffer() */ +smp_mb ();/* spin_unlock may have inclusive semantics */ +if (waitqueue_active (&bh->b_wait)) +wake_up (&bh->b_wait); +switch (rw) {case WRITE : kstat.pgpgout += count;
fs/buffer.c +21 -1
@@ -153,10 +153,23 @@ void __wait_on_buffer(struct buffer_headget_bh (bh);add_wait_queue (&bh->b_wait, &wait);do { -run_task_queue (&tq_disk);set_task_state (tsk,TASK_UNINTERRUPTIBLE );if (!buffer_locked (bh))break ; +/* +* We must read tq_disk in TQ_ACTIVE after the +* add_wait_queue effect is visible to other cpus. +* We could unplug some line above it wouldn't matter +* but we can't do that right after add_wait_queue +* without an smp_mb() in between because spin_unlock +* has inclusive semantics. +* Doing it here is the most efficient place so we +* don't do a suprious unplug if we get a racy +* wakeup that make buffer_locked to return 0, and +* doing it here avoids an explicit smp_mb() we +* rely on the implicit one in set_task_state. +*/ +run_task_queue (&tq_disk);schedule (); }while (buffer_locked (bh)); tsk->state =TASK_RUNNING ;
@@ -1508,6 +1521,9 @@ static int __block_write_full_page(struc/* Done - end_buffer_io_async will unlock */ SetPageUptodate (page); + +wakeup_page_waiters (page); +return 0 ; out:
@@ -1539,6 +1555,7 @@ out: }while (bh != head);if (need_unlock)UnlockPage (page); +wakeup_page_waiters (page);return err; }
@@ -1755,6 +1772,8 @@ int block_read_full_page(struct page *paelse submit_bh (READ , bh); } + +wakeup_page_waiters (page);return 0 ; }
@@ -2368,6 +2387,7 @@ int brw_page(int rw, struct page *page,submit_bh (rw, bh); bh = next; }while (bh != head); +wakeup_page_waiters (page);return 0 ; }
fs/reiserfs/inode.c +1 -0
@@ -1993,6 +1993,7 @@ static int reiserfs_write_full_page(stru*/ if (nr) {submit_bh_for_writepage (arr, nr) ; +wakeup_page_waiters (page); }else {UnlockPage (page) ; }
include/linux/pagemap.h +2 -0
@@ -97,6 +97,8 @@ static inline void wait_on_page(struct p___wait_on_page (page); } +extern void wakeup_page_waiters (struct page * page); +/* * Returns locked page at given index in given cache, creating it if needed. */
kernel/ksyms.c +1 -0
@@ -293,6 +293,7 @@ EXPORT_SYMBOL(filemap_fdatasync);EXPORT_SYMBOL (filemap_fdatawait);EXPORT_SYMBOL (lock_page);EXPORT_SYMBOL (unlock_page); +EXPORT_SYMBOL (wakeup_page_waiters);/* device registration */ EXPORT_SYMBOL (register_chrdev);
mm/filemap.c +14 -0
@@ -909,6 +909,20 @@ void lock_page(struct page *page) }/* +* This must be called after every submit_bh with end_io +* callbacks that would result into the blkdev layer waking +* up the page after a queue unplug. +*/ +void wakeup_page_waiters (struct page * page) +{ +wait_queue_head_t * head; + + head =page_waitqueue (page); +if (waitqueue_active (head)) +wake_up (head); +} + +/* * a rather lightweight function, finding and getting a reference to a * hashed page atomically. */
drivers/block/ll_rw_blk.c +8 -4
@@ -1281,12 +1281,13 @@ static struct request *get_request_wait(spin_lock_prefetch (q->queue_lock); -generic_unplug_device (q); do { prepare_to_wait_exclusive(&rl->wait, &wait, TASK_UNINTERRUPTIBLE); - if (!rl->count) + if (!rl->count){ + generic_unplug_device(q); io_schedule(); + } finish_wait(&rl->wait, &wait); spin_lock_irq(q->queue_lock); rq = get_request(q, rw);
@@ -1487,8 +1488,11 @@ void __blk_put_request(request_queue_t * rl->count++;if (rl->count >=queue_congestion_off_threshold ())clear_queue_congested (q, rw); -if (rl->count >= batch_requests &&waitqueue_active (&rl->wait)) -wake_up (&rl->wait); +if (rl->count >= batch_requests) { +smp_mb (); +if (waitqueue_active (&rl->wait)) +wake_up (&rl->wait); + } } }
fs/buffer.c +15 -4
@@ -135,9 +135,10 @@ void __wait_on_buffer(struct buffer_headget_bh (bh);do {prepare_to_wait (wqh, &wait,TASK_UNINTERRUPTIBLE ); -blk_run_queues (); -if (buffer_locked (bh)) +if (buffer_locked (bh)) { +blk_run_queues ();schedule (); + } }while (buffer_locked (bh));put_bh (bh);finish_wait (wqh, &wait);
@@ -1727,7 +1728,8 @@ done:if (uptodate)SetPageUptodate (page);end_page_writeback (page); - } + }else +wakeup_page_waiters (page);if (err ==0 )return ret;return err;
@@ -2011,6 +2013,7 @@ int block_read_full_page(struct page *paelse submit_bh (READ , bh); } +wakeup_page_waiters (page);return 0 ; }
@@ -2315,6 +2318,8 @@ static int end_bio_bh_io_sync(struct bioint submit_bh (int rw,struct buffer_head * bh) {struct bio *bio; +int ret; +wait_queue_head_t *wqh =bh_waitq_head (bh);BUG_ON (!buffer_locked (bh));BUG_ON (!buffer_mapped (bh));
@@ -2348,7 +2353,13 @@ int submit_bh(int rw, struct buffer_head bio->bi_end_io = end_bio_bh_io_sync; bio->bi_private = bh; -return submit_bio (rw, bio); + ret =submit_bio (rw, bio); + +smp_mb ();/* spin_unlock may have inclusive semantics */ +if (waitqueue_active (wqh)) +wake_up (wqh); + +return ret; }/**
fs/reiserfs/inode.c +1 -0
@@ -1987,6 +1987,7 @@ static int reiserfs_write_full_page(stru*/ if (nr) {submit_bh_for_writepage (arr, nr) ; +wakeup_page_waiters (page); }else {end_page_writeback (page) ; }
include/linux/pagemap.h +3 -0
@@ -122,4 +122,7 @@ static inline void wait_on_page_writebac }extern void end_page_writeback (struct page *page); + +extern void wakeup_page_waiters (struct page * page); +#endif /* _LINUX_PAGEMAP_H */
mm/filemap.c +20 -4
@@ -272,9 +272,10 @@ void wait_on_page_bit(struct page *page,do {prepare_to_wait (waitqueue, &wait,TASK_UNINTERRUPTIBLE ); -sync_page (page); -if (test_bit (bit_nr, &page->flags)) +if (test_bit (bit_nr, &page->flags)) { +sync_page (page);io_schedule (); + } }while (test_bit (bit_nr, &page->flags));finish_wait (waitqueue, &wait); }
@@ -336,15 +337,30 @@ void __lock_page(struct page *page)while (TestSetPageLocked (page)) {prepare_to_wait (wqh, &wait,TASK_UNINTERRUPTIBLE ); -sync_page (page); -if (PageLocked (page)) +if (PageLocked (page)) { +sync_page (page);io_schedule (); + } }finish_wait (wqh, &wait); }EXPORT_SYMBOL (__lock_page);/* +* This must be called after every submit_bh with end_io +* callbacks that would result into the blkdev layer waking +* up the page after a queue unplug. +*/ +void wakeup_page_waiters (struct page * page) +{ +wait_queue_head_t * wqh; + + wqh =page_waitqueue (page); +if (waitqueue_active (wqh)) +wake_up (wqh); +} + +/* * a rather lightweight function, finding and getting a reference to a * hashed page atomically. */
mm/page_io.c +2 -0
@@ -104,6 +104,7 @@ int swap_writepage(struct page *page)SetPageWriteback (page);unlock_page (page);submit_bio (WRITE , bio); +wakeup_page_waiters (page); out:return ret; }
@@ -121,6 +122,7 @@ int swap_readpage(struct file *file, str }inc_page_state (pswpin);submit_bio (READ , bio); +wakeup_page_waiters (page); out:return ret; }
kernel/ksyms.c renamed from kernel/ksyms.c.~1~ +1 -0
@@ -336,6 +336,7 @@ EXPORT_SYMBOL(filemap_fdatawrite);EXPORT_SYMBOL (filemap_fdatawait);EXPORT_SYMBOL (lock_page);EXPORT_SYMBOL (unlock_page); +EXPORT_SYMBOL (wakeup_page_waiters);/* device registration */ EXPORT_SYMBOL (register_chrdev);
Quoted 14 lines · Andrea Arcangeli
the race looks like this:
CPU0 CPU1 ----------------- ------------------------ reiserfs_writepage lock_buffer() fsync_buffers_list() under lock_super() wait_on_buffer() run_task_queue(&tq_disk) -> noop schedule() <- hang with lock_super acquired submit_bh() /* don't unplug here */
Or, more simply:
lock_buffer()
while (buffer_locked()) {
blk_run_queues(); /* Nothing happens */
if (buffer_locked(bh))
schedule();
submit_bh(); /* No unplug */The fix seems reasonable to me. It would perhaps be better to just do:
+ if (waitqueue_active(wqh))
+ blk_run_queues();
in submit_bh(). To save the context switch.
Moving the blk_run_queues() inside the TASK_UNINTERRUPTIBLE region is something which always worried me, because if something down there sets TASK_RUNNING, we end up in a busy wait. But that's OK for 2.5 and may be OK for 2.4's run_task_queue() - I haven't checked...
The multipage stuff in 2.5 does its own blk_run_queues() and looks to be OK, which I assume is why you didn't touch that.
The little single-page reads like do_generic_mapping_read() look to be OK because the process whcih waits is the one which submitted the IO.
wrt the get_request_wait changes: I never bothered about the barrier because we know that there are tons of requests outstanding, and if we don't do a wakeup the next guy will. Plus *this* request has to be put back sometime too, which will deliver a wakeup. But whatever; it's not exactly a fastpath.
However the function is still not watertight:
static struct request *get_request_wait(request_queue_t *q, int rw)
{
DEFINE_WAIT(wait);
struct request_list *rl = &q->rq[rw];
struct request *rq;
spin_lock_prefetch(q->queue_lock);
generic_unplug_device(q);
do {
prepare_to_wait_exclusive(&rl->wait, &wait,
TASK_UNINTERRUPTIBLE);
if (!rl->count)
io_schedule();
finish_wait(&rl->wait, &wait);
spin_lock_irq(q->queue_lock);
rq = get_request(q, rw);
spin_unlock_irq(q->queue_lock);
} while (rq == NULL);
return rq;
}If someone has taken *all* the requests and hasn't submitted any of them
yet, there is nothing to unplug. We go to sleep, all the requests are
submitted (behind a plug) and it's game over. Could happen if the device has a teeny queue...
I dunno. I bet there are still more holes, and I for one am heartily sick of unplug bugs. Why not make the damn queue unplug itself after ten milliseconds or 16 requests? I bet that would actually increase throughput, especially in the presence of kernel preemption and scheduling points.
Quoted 16 lines · Andrew Morton
Quoted 14 lines · Andrea Arcangeli
the race looks like this:
CPU0 CPU1 ----------------- ------------------------ reiserfs_writepage lock_buffer() fsync_buffers_list() under lock_super() wait_on_buffer() run_task_queue(&tq_disk) -> noop schedule() <- hang with lock_super acquired submit_bh() /* don't unplug here */
Quoted 9 lines
Or, more simply:
lock_buffer() while (buffer_locked()) { blk_run_queues(); /* Nothing happens */ if (buffer_locked(bh)) schedule(); submit_bh(); /* No unplug */
yep that is the simple one, I mentioned the reiserfs more complicated special case since it is the one that allowed this bug to see the light of the day, the simple race is resolved by kupdate in mean after 2.5 sec of hang time.
Quoted 7 lines
The fix seems reasonable to me. It would perhaps be better to just do:
(quoted diff) +2
-0+ if (waitqueue_active(wqh)) + blk_run_queues();in
submit_bh(). To save the context switch.
the race triggers very rarely I doubt it could make any difference to save the context switch and wake_up looked simpler conceptually, but run_task_queue(&tq_disk)/blk_run_queues will fix it too indeed. It's up to you, I find the wake_up cleaner.
something more important if you care about microoptimizations is probably to add unlikely(waitqueue_active()).
Quoted 4 lines
Moving the
blk_run_queues()inside theTASK_UNINTERRUPTIBLEregion is something which always worried me, because if something down there setsTASK_RUNNING, we end up in a busy wait. But that's OK for 2.5 and may be OK for 2.4'srun_task_queue()- I haven't checked...
even in the unlikely case some buggy request_fn sets the task to runnable, the second invocation of run_task_queue will do nothing, so there's no risk to hang in R state and that is the only safe place to unplug, unplugging before setting the task state is totally broken and racy, see the deadlock explanation in get_request_wait in 2.4/2.5.
Quoted 2 lines
The multipage stuff in 2.5 does its own
blk_run_queues()and looks to be OK, which I assume is why you didn't touch that.
yes, but also because I didn't looked much at the 2.5 code, so you should double check there, at the moment I care most about 2.4 where this is been a blocker for us until a few days ago.
Quoted 2 lines
The little single-page reads like
do_generic_mapping_read()look to be OK because the process whcih waits is the one which submitted the IO.
no, you forgot readahead, that's buggy and needs fixing in readpage too (same in 2.4 and 2.5).
Quoted 5 lines
wrt the
get_request_waitchanges: I never bothered about the barrier because we know that there are tons of requests outstanding, and if we don't do a wakeup the next guy will. Plus *this* request has to be put back sometime too, which will deliver a wakeup. But whatever; it's not exactly a fastpath.
I assume you're talking about the smp_mb() needed before reading the waitqueue. I'm not claiming that the race is pratical, it may not trigger in most hardware due the timings and hardware detail, but it can definitely happen in theory.
It doesn't matter if we know there are tons of requests outstanding, if we read the waitqueue before increasing count, we won't wakeup the other cpu, and we could get a flood of I/O completion in all cpus all watching at the waitqueue first, you simply need tot_requests-batch_requests == nr_cpus - 1 to be able to trigger it in practice. So algorithmically speaking the smp_mb() (i.e. barrier() in UP) is needed.
Quoted 24 lines
However the function is still not watertight:
static struct request *get_request_wait(request_queue_t *q, int rw)
{ DEFINE_WAIT(wait); struct request_list *rl = &q->rq[rw]; struct request *rq; spin_lock_prefetch(q->queue_lock); generic_unplug_device(q); do { prepare_to_wait_exclusive(&rl->wait, &wait, TASK_UNINTERRUPTIBLE); if (!rl->count) io_schedule(); finish_wait(&rl->wait, &wait); spin_lock_irq(q->queue_lock); rq = get_request(q, rw); spin_unlock_irq(q->queue_lock); } while (rq == NULL); return rq; }
Quoted 4 lines
If someone has taken *all* the requests and hasn't submitted any of them
yet, there is nothing to unplug. We go to sleep, all the requests are
submitted (behind a plug) and it's game over. Could happen if the device has a teeny queue...
I thought the queue spinlock would avoid that but I overlooked get_request_wait itself against another get_request_wait, so yes that is still an open problem, nevertheless my fix to get_request_queue to unplug after being in the waitqueue and in uninterruptable mode is still definitely needed and it fixes another bug that could deadlock get_request_wait too (actually a more severe bug than this subtle one).
I will think more on how to fix get_request_wait against get_request_wait. One simple fix of course is to never release the spinlock between the get_request() and the add_request(). This is infact what I thought would been always the case but I forgotten get_request_wait against get_request_wait as said. In short to fix this cleanly (without wakeups [or suprious unplugs if you prefer]) we should never drop the q->queue_lock with the request not in the waitqueue and not in the freelist.
Quoted 4 lines
I dunno. I bet there are still more holes, and I for one am heartily sick of unplug bugs. Why not make the damn queue unplug itself after ten milliseconds or 16 requests? I bet that would actually increase throughput, especially in the presence of kernel preemption and scheduling points.
This doesn't remove the need to avoid the mean 5 msec delay before the queue unplug (I know the race triggers rarely though, but if we left all the places broken the number of 5msec mean waits will increase bug by bug over time). I'm not very excited by the idea (in particular for 2.4), if it performs so much better I would say it's a broken length of the queue that is way too oversized and that leads to other problems with fariness of task against task etc... We need the big number of requests with small requests only, with big requests we should allow only very few requests to be returned.
In short I think my patch is correct and needed in both 2.4 and 2.5. I will now think on how to fix best the get_request_wait against get_request_wait that you noticed too and I will upload an incremental fix for it. I would suggest to merge the current fixes in the meantime (if the mainline maintainers prefers to change it and unplug instead of waking up the waiter that's ok with me, conceptually I find cleaner to wakeup the waiter but that's up to you)
Thanks for the help,
Andrea
PS. CC'ed Chris, he's certainly interested too given he found the get_request_wait problem. Now that I think about it, I guess Chris really meant the get_request_wait against get_request_wait that you found too above, I probably just happened to notice the simpler more severe bug in such function and I thought Chris meant that simpler one too, not exactly the same race as in lock_buffer/lock_page infact, and I didn't imagine get_request_wait was really buggy in two different ways ;).
Quoted 4 lines · Andrea Arcangeli
no, you forgot readahead, that's buggy and needs fixing in readpage too (same in 2.4 and 2.5).
readahead in 2.5 unplugs. Which is quite wrong for AIO, but is unavoidable given the current situation...
Quoted 7 lines
...
Quoted 4 lines
I dunno. I bet there are still more holes, and I for one am heartily sick of unplug bugs. Why not make the damn queue unplug itself after ten milliseconds or 16 requests? I bet that would actually increase throughput, especially in the presence of kernel preemption and scheduling points.
Quoted 9 lines
This doesn't remove the need to avoid the mean 5 msec delay before the queue unplug (I know the race triggers rarely though, but if we left all the places broken the number of 5msec mean waits will increase bug by bug over time). I'm not very excited by the idea (in particular for 2.4), if it performs so much better I would say it's a broken length of the queue that is way too oversized and that leads to other problems with fariness of task against task etc... We need the big number of requests with small requests only, with big requests we should allow only very few requests to be returned.
No, no, no. Step back, miles back. Forget the current implementation and ask yourself "what is a good design for plugging?". Because I can assure you that if someone came out and proposed the current design for inclusion in today's kernel, they would be pulverised.
Plugging is an optimisation which is specific to, and internal to the request layer. It should not require that every client of that layer know about it, and be careful to unplug (with bizarrely complex locking and ordering rules) simply because the request layer is too lame to look after its own stuff.
Particularly because the clients of the request layer do not have enough information to make a good decision about when to unplug. Only the request layer knows that.
And particularly because we've been hitting the same darn bug for years. And we've been blaming the poor old callers!
The request layer should perform its own unplug "when the time is right", and the external unplug should be viewed as an optimisation hint.
It all becomes rather obvious when you try to answer the question "when should the kernel unplug for AIO reads"?
And yes, the fact that the time-based component of the unplug decision would obscure failure by callers to make use of the optimisation hint is unfortunate. But it's only a few callsites, and 99.9999% coverage there is good enough.
Quoted 8 lines · Andrew Morton
Quoted 4 lines · Andrea Arcangeli
no, you forgot readahead, that's buggy and needs fixing in readpage too (same in 2.4 and 2.5).
readahead in 2.5 unplugs. Which is quite wrong for AIO, but is unavoidable given the current situation...
Current situation?? I fixed these bugs and you certainly didn't know about them previously. Furthmore writeback won't unplug or it would suck totally period, so you still need it at the page layer too.
Quoted 36 lines
Quoted 5 lines
Quoted 4 lines
I dunno. I bet there are still more holes, and I for one am heartily sick of unplug bugs. Why not make the damn queue unplug itself after ten milliseconds or 16 requests? I bet that would actually increase throughput, especially in the presence of kernel preemption and scheduling points.
Quoted 9 lines
This doesn't remove the need to avoid the mean 5 msec delay before the queue unplug (I know the race triggers rarely though, but if we left all the places broken the number of 5msec mean waits will increase bug by bug over time). I'm not very excited by the idea (in particular for 2.4), if it performs so much better I would say it's a broken length of the queue that is way too oversized and that leads to other problems with fariness of task against task etc... We need the big number of requests with small requests only, with big requests we should allow only very few requests to be returned.
No, no, no. Step back, miles back. Forget the current implementation and ask yourself "what is a good design for plugging?". Because I can assure you that if someone came out and proposed the current design for inclusion in today's kernel, they would be pulverised.
Plugging is an optimisation which is specific to, and internal to the request layer. It should not require that every client of that layer know about it, and be careful to unplug (with bizarrely complex locking and ordering rules) simply because the request layer is too lame to look after its own stuff.
Particularly because the clients of the request layer do not have enough information to make a good decision about when to unplug. Only the request layer knows that.
And particularly because we've been hitting the same darn bug for years. And we've been blaming the poor old callers!
The request layer should perform its own unplug "when the time is right", and the external unplug should be viewed as an optimisation hint.
Quoted 7 lines
It all becomes rather obvious when you try to answer the question "when should the kernel unplug for AIO reads"?
And yes, the fact that the time-based component of the unplug decision would obscure failure by callers to make use of the optimisation hint is unfortunate. But it's only a few callsites, and 99.9999% coverage there is good enough.
so what? you agree the hint is needed and if you don't fix the race it's like no hint at all and you wait 5 msec in mean which I don't want to, period. Ok 5 msec isn't too bad (yeah also the current 2.5sec hangs with ext2 aren't too bad) but they're still a not too bad _bugs_. The more not too bad bugs you add the more the things will start to get worse and worse.
then tell me how to unplug from a timer irq, with a context switch, not nice.
Anyways I think the answer to the question "when to unplug" has nothing to do with these races, when wait_on_buffer arrives and says "I need this I/O completed ASAP" that request must arrive to the blkdev layer, no matter how and no matter if it would lead to a deadlock or not.
We have bugs in delivering the "I need this I/O completed ASAP" message, not in the "when to unplug" algorithm. You're arguing about a totally different thing. Incidentally now the two things tends to overlap but that's just incidental and IMHO it is very an efficient solution, more than doing a context switch at every unplug like you're suggesting, most probably to hide an overkill size of some I/O queue. We just have a background unplug every 5 sec (unless the system hangs for bugs in other places), if there are only a few requests in the queue that's fine to delay them every 5 sec. So in short I think the current logic is just fine, feel free to prove me wrong send me a patch and I will benchmark it, I only reserve the possibility of fixing the blkdev layer in case your patch would workaround any stupid thing there.
Last but not the least by your argument that these fixes are useless I could as well fix our hang completely by simply adding a new kernel thread doing schedule_timeout(1); run_queue_task(&tq_disk) in a loop, if I would ever remotely do anything like that you would be flaming me for not fixing the real bug, and still I would suffer from 5 msec slowdowns in the buggy places, just like your universal unplug solution suffers too.
Not fixing these races would be very bad, I'm totally stunned that you're against these fixes and you propose to hide these bugs and to make the system worse and worse over time.
Andrea
Quoted 1 line · Andrea Arcangeli
I agreed that the fix was reasonable, and then went on to discuss broader things.