On Fri, Aug 25, 2017 at 7:54 PM, Linus Torvalds <torvalds@xxxxxxxxxxxxxxxxxxxx> wrote: > > Simplify, simplify, simplify. I've now tried three different approaches (I stopped sending broken patches after deciding the first one was unfixable), and they all suck. I was hoping for something lockless to avoid the whole issue with latency over the long list, but anything that has the wait queue entry allocated on the stack needs to synchronize the wakeup due to the stack usage, so once you have lists that are thousands of entries, either you hold the lock for long times (normal wait queues) or take and release them constantly (the swait approach), or you batch things up (Tim's wait-queue patches). Of those three approaches, the batching does seem to be the best of the lot. Allocating non-stack wait entries while waiting for pages seems like a bad idea. We're probably waiting for IO in normal circumstances, possibly because we're low on memory. So I *am* ok with the batching (your patch #1), but I'm *not* ok with the nasty horrible book-keeping to avoid new entries when you batch and release the lock and that allows new entries on the list (your patch #2). That said, I have now stared *way* too much at the page wait code after having unsuccessfully tried to replace that wait-queue with other "clever" approaches (all of which ended up being crap). And I'm starting to see a possible solution, or at least improvement. Let's just assume I take the batching patch. It's not conceptually ugly, it's fairly simple, and there are lots of independent arguments for it (ie latency, but also possible performance from better parallelism). That patch doesn't make any data structures bigger or more complicated, it's just that single "is this a bookmark entry" thing. So that patch just looks fundamentally fine to me, and the only real argument I ever had against it was that I would really really _really_ have wanted to root-cause the behavior. So that leaves my fundamental dislike of your other patch. And it turns out that all my looking at the page wait code wasn't entirely unproductive. Yes, I went through three crap approaches before I gave up on rewriting it, but in the meantime I did get way too intimate with looking at that pile of crud. And I think I found the reason why you needed that patch #2 in the first place. Here's what is going on: - we're going to assume that the problem is all with a single page, not due to hash collisions (as per your earlier reports) - we also know that the only bit that matters is the PG_locked bit - that means that the only way to get that "multiple concurrent waker" situation that your patch #2 tries to handle better is because you have multiple people unlocking the page - since that is what causes the wakeups - that in turn means that you obviously had multiple threads *locking* it too. - and that means that among those new entries there are lockers coming in in the middle and adding an exclusive entry. - the exclusive entry already stops the wakeup list walking - but we add non-exclusive entries TO THE BEGINNING of the page waiters list And I really think that last thing is fundamentally wrong. It's wrong for several reasons: - because it's unfair: threads that want to lock get put behind threads that just want to see the unlocked state. - because it's stupid: our non-locking waiters will end up waiting again if the page got locked, so waking up a locker *and* a lot of non-locking waiters will just cause them to go back to sleep anyway - because it causes us to walk longer lists: we stop walking when we wake up the exclusive waiter, but because we always put the non-exclusive waiters in there first, we always walk the long list of non-exclusive waiters even if we could just stop walking because we woke up an exclusive one. Now the reason we do this seems to be entirely random: for a *normal* wait queue, you really want to always wake up all the non-exclusive waiters, because exclusive waiters are only exclusive wrt each other. But for a page lock, an exclusive waiter really is getting the lock, and the other waiters are going to wait for the lock to clear anyway, so the page wait thing is actually almost exactly the reverse situation. We *could* put exclusive waiters at the beginning of the list instead, and actively try to avoid walking the list at all if we have pending lockers. I'm not doing that, because I think the fair thing to do is to just do things in the order they came in. Plus the code is actually simpler if we just always add to the tail. Now, the other thing to look at is how the wakeup function works. It checks the aliasing information (page and bit number), but then it *also* does: if (test_bit(key->bit_nr, &key->page->flags)) return 0; basically saying "continue walking if somebody else already got the bit". That's *INSANE*. It's exactly the wrong thing to do. It's basically saying "even if we had an exclusive waiter, let's not wake it up, but do let us continue to walk the list even though the bit we're waiting for to clear is set already". What would be sane is to say "stop walking the list now".. So just do that - by making "negative return from wake function" mean "stop walking". So how about just this fairly trivial patch? In fact, I think this may help even *without* Tim's patch #1. So I think this would be lovely to test on that problem load on its own, and seeing if it makes the wait queues behave better. It might not cut down on the total length of the wait-queue, but it should hopefully cause it to walk it much less. We now hit the exclusive waiters earlier and stop waking things up when we have a new locker thread pending. And when the page ends up being locked again, we stop walking the list entirely, so no unnecessarily traversal. This patch is small and at least minimally works (I'm running it right now). Linus
kernel/sched/wait.c | 7 ++++--- mm/filemap.c | 10 +++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/kernel/sched/wait.c b/kernel/sched/wait.c index 17f11c6b0a9f..d6afed6d0752 100644 --- a/kernel/sched/wait.c +++ b/kernel/sched/wait.c @@ -70,9 +70,10 @@ static void __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode, list_for_each_entry_safe(curr, next, &wq_head->head, entry) { unsigned flags = curr->flags; - - if (curr->func(curr, mode, wake_flags, key) && - (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive) + int ret = curr->func(curr, mode, wake_flags, key); + if (ret < 0) + break; + if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive) break; } } diff --git a/mm/filemap.c b/mm/filemap.c index a49702445ce0..60705b760983 100644 --- a/mm/filemap.c +++ b/mm/filemap.c @@ -909,8 +909,10 @@ static int wake_page_function(wait_queue_entry_t *wait, unsigned mode, int sync, if (wait_page->bit_nr != key->bit_nr) return 0; + + /* Stop walking if it's locked */ if (test_bit(key->bit_nr, &key->page->flags)) - return 0; + return -1; return autoremove_wake_function(wait, mode, sync, key); } @@ -964,6 +966,7 @@ static inline int wait_on_page_bit_common(wait_queue_head_t *q, int ret = 0; init_wait(wait); + wait->flags = lock ? WQ_FLAG_EXCLUSIVE : 0; wait->func = wake_page_function; wait_page.page = page; wait_page.bit_nr = bit_nr; @@ -972,10 +975,7 @@ static inline int wait_on_page_bit_common(wait_queue_head_t *q, spin_lock_irq(&q->lock); if (likely(list_empty(&wait->entry))) { - if (lock) - __add_wait_queue_entry_tail_exclusive(q, wait); - else - __add_wait_queue(q, wait); + __add_wait_queue_entry_tail(q, wait); SetPageWaiters(page); }