Re: [PATCH mm-next] mm/swap: free swap cache folios the zap path gave up on
From: Jiayuan Chen
Date: Mon Oct 19 2026 - 22:39:57 EST
On 10/20/26 2:08 AM, Nhat Pham wrote:
On Mon, Oct 19, 2026 at 7:37 AM Jiayuan Chen <jiayuan.chen@xxxxxxxxx> wrote:[...]
Zap drops swap count first, then try to free swap cache. You're sayingAt that point the swap count is still 1; the zap comes while the readRight.. But I think it will be much better to avoid such folios fromAlso, IIRC, dropbehind can only be handled in task context, right?
hunging in the swap cache in the first place instead of having a
worker to reclaim them. I'm not strongly against adding a reclaimer if
other approaches are too difficult at this point though.
We'd need to make readahead bios to complete in task context
(BIO_COMPLETE_IN_TASK). I wonder if this has any perf implications.
Another idea: how about, in swap_read_folio(), where we are guaranteed
to have the folio's lock, we check if folio is mapped. If it's not,
invoke folio_free_swap(), unlock, put, then drop that folio from the
submit batch?
is in flight. Checking after the read works for the synchronous zswap
path, which I tried first, but the bdev read is unlocked in
swap_bio_read_end_io(), in interrupt context, where the slot cannot be
freed.
the read check the swap count before zap dropping it, but still holds
the lock by the time zap try to free swap cache? :(
Yes, exactly that. With the check in swap_read_folio():
thread A (readahead) thread B
swap_cache_alloc_folio(B's entry)
folio locked, added to cache
swap count 1 (B's PTE)
swap_read_folio()
folio_free_swap()
count is 1, nothing to do
zswap_load() / submit_bio()
. MADV_DONTNEED
. swap count 1 -> 0
. __try_to_reclaim_swap()
. folio_trylock() fails
. gives up
read completes
folio_unlock()
folio_put()
-> count 0, folio in swap cache, unmapped
The check runs before the zap, so it sees count 1. The zap comes
while A holds the lock for the read.
Checking after the read instead is what I tried first. It works for
the synchronous path and not for the bdev one:
zswap (sync) bdev (async)
swap_read_folio() swap_read_folio()
zswap_load() submit_bio()
. <- zap, count 1 -> 0 return <- zap, count 1 -> 0
read done, lock still held ...
count is 0: free the slot, fine swap_bio_read_end_io() (irq)
folio_unlock() folio_unlock()
no ci->lock here, slot stays
The write side is the same picture with swap_writeout() and
end_swap_bio_write().
I was kinda hoping this kind of interleaving would be rare :(
It is rare per event, but it adds up. On production it takes days to
leave a few GiB behind. The DONTNEED there comes from jemalloc, which
issues it on every free the application does, so it never stops. Our
reproducer is the extreme case, built only to verify the problem: it
leaves 480 GB in about an hour. I'll put the numbers in the commit
message.
And yeah, I think if we want to free the slot after read, readahead
swap IOs have to be submitted with BIO_COMPLETE_IN_TASK. zswap
writeback's dropbehind relied on that...
Right, and that would still leave the writeback side. The worker covers both in one place.
Yeah I got no smarter ideas :( sweep_work it is...
That takes care of the readahead race. Seems safe to meSame shape: the zap arrives after that check, while the folio is under
(I assume if a folio is not mapped and swap count == 0, then it's a
pure readahead artifact, but maybe I'm missing a case).
The write direction already tries folio_free_swap() before submitting
the writeback, FWIW.
writeback. On our box with a swap partition that is where most of the
leftover came from.
Every check on the holder's side runs while the count is still 1. Only
the zap sees it go to 0, and it cannot wait. That is why the patch has
the zap hand it over instead (very nasty i know it :( ).