lkml.org 
[lkml]   [2023]   [May]   [29]   [last100]   RSS Feed
Views: [wrap][no wrap]   [headers]  [forward] 
 
Messages in this thread
/
Date
From
SubjectRe: [PATCH v4] mm, compaction: Skip all non-migratable pages during scan
On 29.05.23 02:31, Matthew Wilcox wrote:
> On Sun, May 28, 2023 at 04:49:52PM -0700, John Hubbard wrote:
>> On 5/26/23 20:18, Matthew Wilcox wrote:
>>> On Fri, May 26, 2023 at 07:11:05PM -0700, John Hubbard wrote:
>>>>> So any user with 1024 processes can fragment physical memory? :/
>>>>>
>>>>> Sorry, I'd like to minimize the usage of folio_maybe_dma_pinned().
>>>>
>>>> I was actually thinking that we should minimize any more cases of
>>>> fragile mapcount and refcount comparison, which then leads to
>>>> Matthew's approach here!
>>>
>>> I was wondering if we shouldn't make folio_maybe_dma_pinned() a little
>>> more accurate. eg:
>>>
>>> if (folio_test_large(folio))
>>> return atomic_read(&folio->_pincount) > 0;
>>> return (unsigned)(folio_ref_count(folio) - folio_mapcount(folio)) >=
>>> GUP_PIN_COUNTING_BIAS;
>>
>> I'm trying to figure out what might be wrong with that, but it seems
>> OK. We must have talked about this earlier, but I recall vaguely that
>> there was not a lot of concern about the case of a page being mapped
>>> 1024 times. Because pinned or not, it's likely to be effectively
>> locked into memory due to LRU effects. As mentioned here, too.
>
> That was my point of view, but David convinced me that a hostile process
> can effectively lock its own memory into place.
>

1) My opinion on this optimization

Before I start going into detail, let me first phrase my opinion so we
are on the same page:

"a tiny fraction of Linux installations makes heavy use of long-term
pinning -- the *single* mechanism that completely *destroys* the whole
purpose of memory compaction -- and the users complain about memory
compaction overhead. So we are talking about optimizing for that by
eventually harming *everybody else*."

Ehm ... I'm all for reasonable optimization, but not at any price.

We don't care about a handful of long-term pinned pages in the system,
this is really about vfio long-term pinning a significant amount of
system RAM, and we only care about shmem here.


*maybe* there is an issue with page migration when we have many page
mappings, but (a) it's a separate issue and to be dealt with separately,
not buried into such checks (b) it's unclear how many page mappings are
too many, the magic number 1024 is just a random number (c) it needs
much finer control (hostile processes).


2) mapcount vs. pagecount

Now, doing these mapcount vs. pagecount checks is perfectly reasonable
(see mm/ksm.c) as long as know what we're doing. For example, we have to
make sure that a possible compound page cannot get split concurrently
(e.g., hold a reference). It's been used forever, I consider it stable.

I completely agree that we should be careful with such mapcount vs.
pagecount checks, and if we can use something better, let's use
something *better*.


3) page_maybe_dma_pinned()

Now, why do I dislike bringing up page_maybe_dma_pinned() [IOW, why is
it not better]? Besides it ignoring FOLL_GET for now, that might be
fixed at some point.

I think we agree that order-0 pages are the problem, because we get
guaranteed false positives with many mappings (not just on speculative
page pinnings). For these order-0 pages, it's perfectly reasonable to
check page_maybe_dma_pinned() *as long as* we know the number of
mappings is very small.

I don't consider anon pages the issue here, we barely get 1024 mappings
(not even with KSM), and it's much harder to exploit because you cannot
simply get new mappings via mmap(), only via fork().

In fact, we could optimize easily for order-0 anon pages if we'd need
to: I have a patch lying around, it just wasn't really worth it for now,
because there is only a single relevant page_maybe_dma_pinned() call in
vmscan that could benefit:

https://github.com/davidhildenbrand/linux/commit/0575860d064694d4e2f307b2c20a880a6a7b59ab

We cannot do the same for pagecache pages, so we would possibly
introduce harm by carelessly checking page_maybe_dma_pinned() on pages
with many mappings.


4) folio_maybe_dma_longterm_pinned() ?

I thought yesterday if we'd want something like
folio_maybe_dma_longterm_pinned() here. Essentially using what we
learned about long-term pinning of fs pages:

(1) ZONE_MOVABLE, MIGRATE_CMA -> "return false;"
(2) If !anon, !hugetlb, !shmem -> "return false;"
(3) "return folio_maybe_dma_pinned()"

Yes, above would easily create false-positives for short-term pinned
pages (anon/hugetlb/shmem), but would never create false-positives for
any other page (shared library ...).


We would use it in the following way:

bool skip_folio_in_isolation()
{
/*
* Avoid skipping pages that are short-term pinned, the pin
* might go away any moment and we'll succeed to migrate.
*
* We get false positives for short-term pinned anon, shmem and
* hugetl pages for now, but such short-term pins are transient.
*/
if (!folio_maybe_dma_longterm_pinned())
return false;
/*
* order-0 pages with many mappings can easily be confused
* for pinned pages and this could be exploited by
* malicious user-space to cause fragmentation. This is only
* an optimization, so if a page (especially shmem) is mapped
* many times, we'll rather try migrating it instead of
* accidentally skipping it all the time.
*/
return folio_order(folio) != 0 || && total_mappings <= 32)
}

Someone long-term pins an shmem page with many mappings? Too bad, we
don't optimize for that and still try migrating it.


BUT, I am still confused if we want to check here for "any additional
references", which is what mapcount vs. refcount is, or
folio_maybe_dma_longterm_pinned().

Of course, we could similarly write a variant of skip_folio_in_isolation:

bool skip_folio_in_isolation()
{
/*
* If a page is not pinned, try migrating it. Note that this
* does not consider any FOLL_GET used for DMA yet.
*/
if (!folio_maybe_dma_pinned())
return false;
/*
* order-0 pages with many mappings can easily be confused
* for pinned pages and this could be exploited by
* malicious user-space to cause fragmentation. This is only
* an optimization, so if a page is mapped
* many times, we'll rather try migrating it instead of
* accidentally skipping it all the time.
*/
return folio_order(folio) != 0 || && total_mappings <= 32)
}


As long as FOLL_GET is still used for DMA, the mapcount vs. pagecount
checks might be better ... but it depends on if we care about short-term
or long-term pinned pages here.

>> Anyway, sure.
>>
>> A detail:
>>
>> The unsigned cast, I'm not sure that helps or solves anything, right?
>> That is, other than bugs, is it possible to get refcount < mapcount?

BUG IMHO.

>>
>> And if it's only due to bugs, then the casting, again, isn't likely to
>> going to mitigate the fallout from whatever mess the bug caused.
>
> I wasn't thinking too hard about the cast. If the caller has the folio
> lock, I don't think it's possible for refcount < mapcount. This caller
> has a refcount, but doesn't hold the lock, so it is possible for them
> to read mapcount first, then have both mapcount and refcount decremented
> and see refcount < mapcount.
>
> I don't think it matters too much. We don't hold the folio lock, so
> it might transition from pinned to unpinned as much as a refcount might
> be decremented or a mapcount incremented. What's important is that a
> hostile process can't prevent memory from being moved indefinitely.
>
> David, have I missed something else?


What I learned from staring at the code in mm/ksm.c:write_protect_page()
for too long a while ago is that:

(1) Mapping a page first increments the refcount, then the mapcount
(2) Unmapping a page first decrements the mapcount, then the refcount

So the mapcount is supposed to be always larger than the refcount.
Especially, if you take a snapshot of both (read first the mapcount,
then the mapcount).

A hostile process wouldn't be able to block compaction here forever,
even if we accidentally would make the wrong call once when deciding
whether to isolate a page. It would work on the next attempt.

That's the difference to page_maybe_dma_pinned(), which can be made to
consistently block compaction.


[sorry for the lengthy mail]

--
Thanks,

David / dhildenb

\
 
 \ /
  Last update: 2023-05-29 11:27    [W:0.182 / U:0.884 seconds]
©2003-2020 Jasper Spaans|hosted at Digital Ocean and TransIP|Read the blog|Advertise on this site