Skip to contentSkip to conversation

s390/mm: Batch PTE updates in lazy MMU mode

PATCH Superseded Alexander Gordeev 5 participants 30 messages Apr 15 – Apr 29

Subsystems (9)

v2 of 8 Superseded Latest is v8 →

lore
Share & tools
lore

Alexander Gordeev

author maintainer
#0 lore raw

v2 cover s390/mm: Batch PTE updates in lazy MMU mode

Hi All!

This is v2 of the batched PTE updates in lazy MMU mode rework.

Changes since v1:
- lazy_mmu_mode_enable_pte() renamed to lazy_mmu_mode_enable_for_pte_range()
- lazy_mmu_mode_enable_for_pte_range() semantics clarified
- some sashiko comments addressed [1] including one bug fix
- patches 2-4 added

Patches 3-6 is the actual s390 rework, while patches 1,2 are a prerequisite that affects the generic code.

Patch 6 needs to be mereged in patch 5 if deemed useful.

This series addresses an s390-specific aspect of how page table entries are modified. In many cases, changing a valid PTE (for example, setting or clearing a hardware bit) requires issuing an Invalidate Page Table Entry (IPTE) instruction beforehand.

A disadvantage of the IPTE instruction is that it may initiate a machine-wide quiesce state. This state acts as an expensive global hardware lock and should be avoided whenever possible.

Currently, IPTE is invoked for each individual PTE update in most code paths. However, the instruction itself supports invalidating multiple PTEs at once, covering up to 256 entries. Using this capability can significantly reduce the number of quiesce events, with a positive impact on overall system performance. At present, this feature is not utilized.

An effort was therefore made to identify kernel code paths that update large numbers of consecutive PTEs. Such updates can be batched and handled by a single IPTE invocation, leveraging the hardware support described above.

A natural candidate for this optimization is page-table walkers that change attributes of memory ranges and thus modify contiguous ranges of PTEs. Many memory-management system calls enter lazy MMU mode while updating such ranges.

This lazy MMU mode can be leveraged to build on the already existing infrastructure and implement a software-level lazy MMU mechanism, allowing expensive PTE invalidations on s390 to be batched.

1. https://sashiko.dev/#/patchset/cover.1774420056.git.agordeev%40linux.ibm.com

Thanks!

Alexander Gordeev (6):
mm: Make lazy MMU mode context-aware
mm/pgtable: Fix bogus comment to clear_not_present_full_ptes() s390/mm: Complete ptep_get() conversion
s390/mm: Make PTC and UV call order consistent s390/mm: Batch PTE updates in lazy MMU mode
s390/mm: Allow lazy MMU mode disabling

 arch/s390/Kconfig               |  11 +
 arch/s390/boot/vmem.c           |  32 +--
 arch/s390/include/asm/hugetlb.h |   2 +-
 arch/s390/include/asm/pgtable.h | 258 ++++++++++++++++----
 arch/s390/mm/Makefile           |   1 +
 arch/s390/mm/hugetlbpage.c      |  12 +-
 arch/s390/mm/ipte_batch.c       | 400 ++++++++++++++++++++++++++++++++
 arch/s390/mm/pageattr.c         |  42 ++--
 arch/s390/mm/pgtable.c          |   8 +-
 arch/s390/mm/vmem.c             |  82 ++++---
 fs/proc/task_mmu.c              |   2 +-
 include/linux/pgtable.h         |  51 +++-
 mm/madvise.c                    |   8 +-
 mm/memory.c                     |   8 +-
 mm/mprotect.c                   |   2 +-
 mm/mremap.c                     |   2 +-
 mm/vmalloc.c                    |   6 +-
 17 files changed, 781 insertions(+), 146 deletions(-)
 create mode 100644 arch/s390/mm/ipte_batch.c
Signature · 1 line
2.51.0
author maintainer
#1 lore raw

PATCH v2 1/6 mm: Make lazy MMU mode context-aware

Lazy MMU mode is assumed to be context-independent, in the sense that it does not need any additional information while operating. However, the s390 architecture benefits from knowing the exact page table entries being modified.

Introduce lazy_mmu_mode_enable_for_pte_range(), which is provided with the process address space and the page table being operated on. This information is required to enable s390-specific optimizations.

The function takes parameters that are typically passed to page- table level walkers, which implies that the span of PTE entries never crosses a page table boundary.

Architectures that do not require such information simply do not need to define the lazy_mmu_mode_enable_for_pte_range() callback.

---
 fs/proc/task_mmu.c      |  2 +-
 include/linux/pgtable.h | 47 +++++++++++++++++++++++++++++++++++++++++
 mm/madvise.c            |  8 +++----
 mm/memory.c             |  8 +++----
 mm/mprotect.c           |  2 +-
 mm/mremap.c             |  2 +-
 mm/vmalloc.c            |  6 +++---
 7 files changed, 61 insertions(+), 14 deletions(-)
fs/proc/task_mmu.c +1 -1
index e091931d7ca1..799db0d7ec8b 100644
@@ -2752,7 +2752,7 @@ static int pagemap_scan_pmd_entry(pmd_t *pmd, unsigned long start,
 		return 0;
 	}
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_for_pte_range(vma->vm_mm, start, end, start_pte);
 
 	if ((p->arg.flags & PM_SCAN_WP_MATCHING) && !p->vec_out) {
 		/* Fast path for performing exclusive WP */
include/linux/pgtable.h +47 -0

59 lines not on this page · raw message

mm/madvise.c +4 -4
index dbb69400786d..7faac3a627ff 100644
@@ -451,7 +451,7 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,
 	if (!start_pte)
 		return 0;
 	flush_tlb_batched_pending(mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_for_pte_range(mm, addr, end, start_pte);
 	for (; addr < end; pte += nr, addr += nr * PAGE_SIZE) {
 		nr = 1;
 		ptent = ptep_get(pte);
@@ -506,7 +506,7 @@ static int madvise_cold_or_pageout_pte_range(pmd_t *pmd,
 				if (!start_pte)
 					break;
 				flush_tlb_batched_pending(mm);
-				lazy_mmu_mode_enable();
+				lazy_mmu_mode_enable_for_pte_range(mm, addr, end, start_pte);
 				if (!err)
 					nr = 0;
 				continue;
@@ -673,7 +673,7 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,
 	if (!start_pte)
 		return 0;
 	flush_tlb_batched_pending(mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_for_pte_range(mm, addr, end, start_pte);
 	for (; addr != end; pte += nr, addr += PAGE_SIZE * nr) {
 		nr = 1;
 		ptent = ptep_get(pte);
@@ -733,7 +733,7 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,
 				if (!start_pte)
 					break;
 				flush_tlb_batched_pending(mm);
-				lazy_mmu_mode_enable();
+				lazy_mmu_mode_enable_for_pte_range(mm, addr, end, pte);
 				if (!err)
 					nr = 0;
 				continue;
mm/memory.c +4 -4
index c65e82c86fed..4c0f266df92a 100644
@@ -1269,7 +1269,7 @@ copy_pte_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma,
 	spin_lock_nested(src_ptl, SINGLE_DEPTH_NESTING);
 	orig_src_pte = src_pte;
 	orig_dst_pte = dst_pte;
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_for_pte_range(src_mm, addr, end, src_pte);
 
 	do {
 		nr = 1;
@@ -1917,7 +1917,7 @@ static unsigned long zap_pte_range(struct mmu_gather *tlb,
 		return addr;
 
 	flush_tlb_batched_pending(mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_for_pte_range(mm, addr, end, start_pte);
 	do {
 		bool any_skipped = false;
 
@@ -2875,7 +2875,7 @@ static int remap_pte_range(struct mm_struct *mm, pmd_t *pmd,
 	mapped_pte = pte = pte_alloc_map_lock(mm, pmd, addr, &ptl);
 	if (!pte)
 		return -ENOMEM;
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_for_pte_range(mm, addr, end, mapped_pte);
 	do {
 		BUG_ON(!pte_none(ptep_get(pte)));
 		if (!pfn_modify_allowed(pfn, prot)) {
@@ -3235,7 +3235,7 @@ static int apply_to_pte_range(struct mm_struct *mm, pmd_t *pmd,
 			return -EINVAL;
 	}
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_for_pte_range(mm, addr, end, mapped_pte);
 
 	if (fn) {
 		do {
mm/mprotect.c +1 -1
index c0571445bef7..a7bfb4516dc5 100644
@@ -233,7 +233,7 @@ static long change_pte_range(struct mmu_gather *tlb,
 		is_private_single_threaded = vma_is_single_threaded_private(vma);
 
 	flush_tlb_batched_pending(vma->vm_mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_for_pte_range(vma->vm_mm, addr, end, pte);
 	do {
 		nr_ptes = 1;
 		oldpte = ptep_get(pte);
mm/mremap.c +1 -1
index 2be876a70cc0..16320242da51 100644
@@ -260,7 +260,7 @@ static int move_ptes(struct pagetable_move_control *pmc,
 	if (new_ptl != old_ptl)
 		spin_lock_nested(new_ptl, SINGLE_DEPTH_NESTING);
 	flush_tlb_batched_pending(vma->vm_mm);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_for_pte_range(mm, old_addr, old_end, old_ptep);
 
 	for (; old_addr < old_end; old_ptep += nr_ptes, old_addr += nr_ptes * PAGE_SIZE,
 		new_ptep += nr_ptes, new_addr += nr_ptes * PAGE_SIZE) {
mm/vmalloc.c +3 -3
index 61caa55a4402..35a23044a969 100644
@@ -108,7 +108,7 @@ static int vmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,
 	if (!pte)
 		return -ENOMEM;
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_for_pte_range(&init_mm, addr, end, pte);
 
 	do {
 		if (unlikely(!pte_none(ptep_get(pte)))) {
@@ -371,7 +371,7 @@ static void vunmap_pte_range(pmd_t *pmd, unsigned long addr, unsigned long end,
 	unsigned long size = PAGE_SIZE;
 
 	pte = pte_offset_kernel(pmd, addr);
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_for_pte_range(&init_mm, addr, end, pte);
 
 	do {
 #ifdef CONFIG_HUGETLB_PAGE
@@ -538,7 +538,7 @@ static int vmap_pages_pte_range(pmd_t *pmd, unsigned long addr,
 	if (!pte)
 		return -ENOMEM;
 
-	lazy_mmu_mode_enable();
+	lazy_mmu_mode_enable_for_pte_range(&init_mm, addr, end, pte);
 
 	do {
 		struct page *page = pages[*nr];
Signature · 1 line
2.51.0
Reviewed-by on v2 1/6
#2 lore raw

Re: [PATCH v2 1/6] mm: Make lazy MMU mode context-aware

Quoted 5 lines · Alexander Gordeev

+/**
+ * lazy_mmu_mode_enable_for_pte_range() - Enable the lazy MMU mode with a speedup hint.
+ * @mm: Address space the pages are mapped into.
+ * @addr: Start address of the range.
+ * @end: End address of the range.

Quoted 8 lines

+ * @ptep: Page table pointer for the first entry.
+ *
+ * Enters a new lazy MMU mode section; if the mode was not already enabled,
+ * enables it and calls arch_enter_lazy_mmu_mode_for_pte_range().
+ *
+ * PTEs that fall within the specified range might observe update speedups.
+ * The PTE range must belong to the specified memory space and not cross
+ * a page table boundary.

Does that mean that all PTEs mapping [addr, end) must belong to the same PTE page? I think the wording should be more specific.

LGTM otherwise:

Quoted 23 lines

+ *
+ * There are no requirements on the order or range completeness of PTE
+ * updates for the specified range.
+ *
+ * Must be paired with a call to lazy_mmu_mode_disable().
+ *
+ * Has no effect if called:
+ * - While paused - see lazy_mmu_mode_pause()
+ * - In interrupt context
+ */

include/linux/pgtable.h +13 -0
+static inline void lazy_mmu_mode_enable_for_pte_range(struct mm_struct *mm,
+		unsigned long addr, unsigned long end, pte_t *ptep)
+{
+	struct lazy_mmu_state *state = &current->lazy_mmu_state;
+
+	if (in_interrupt() || state->pause_count > 0)
+		return;
+
+	VM_WARN_ON_ONCE(state->enable_count == U8_MAX);
+
+	if (state->enable_count++ == 0)
+		arch_enter_lazy_mmu_mode_for_pte_range(mm, addr, end, ptep);
+}
author maintainer
#3 lore raw

Re: [PATCH v2 1/6] mm: Make lazy MMU mode context-aware

Quoted 15 lines · Kevin Brodsky
Quoted 5 lines · Alexander Gordeev

+/**
+ * lazy_mmu_mode_enable_for_pte_range() - Enable the lazy MMU mode with a speedup hint.
+ * @mm: Address space the pages are mapped into.
+ * @addr: Start address of the range.
+ * @end: End address of the range.

Quoted 8 lines

+ * @ptep: Page table pointer for the first entry.
+ *
+ * Enters a new lazy MMU mode section; if the mode was not already enabled,
+ * enables it and calls arch_enter_lazy_mmu_mode_for_pte_range().
+ *
+ * PTEs that fall within the specified range might observe update speedups.
+ * The PTE range must belong to the specified memory space and not cross
+ * a page table boundary.

Quoted 2 lines

Does that mean that all PTEs mapping [addr, end) must belong to the same PTE page? I think the wording should be more specific.

I tried to state that end of the range must not exceed pmd_addr_end(addr, end). Any of these sounds better?

The PTE range must belong to the specified memory space and ...
a) the address range must not cross the parent PMD address range boundary
b) the PTEs must belong to the same parent PMD

Quoted 3 lines

LGTM otherwise:

Thanks!

#4 lore raw

Re: [PATCH v2 1/6] mm: Make lazy MMU mode context-aware

Quoted 11 lines · Alexander Gordeev
Quoted 5 lines
Quoted 3 lines

+ * PTEs that fall within the specified range might observe update speedups.
+ * The PTE range must belong to the specified memory space and not cross
+ * a page table boundary.

Does that mean that all PTEs mapping [addr, end) must belong to the same PTE page? I think the wording should be more specific.

I tried to state that end of the range must not exceed pmd_addr_end(addr, end). Any of these sounds better?

The PTE range must belong to the specified memory space and ...
a) the address range must not cross the parent PMD address range boundary
b) the PTEs must belong to the same parent PMD

I think b) is good, maybe "PMD entry" to be even clearer.  My previous suggestion of talking about "PTE page" isn't ideal since some architectures have page tables smaller (or larger) than a page.

- Kevin

#5 lore raw

Re: [PATCH v2 1/6] mm: Make lazy MMU mode context-aware

Quoted 6 lines · Kevin Brodsky
Quoted 5 lines · Alexander Gordeev
Quoted 2 lines

Does that mean that all PTEs mapping [addr, end) must belong to the same PTE page? I think the wording should be more specific.

I tried to state that end of the range must not exceed pmd_addr_end(addr, end). Any of these sounds better?

Quoted 7 lines
Quoted 3 lines

The PTE range must belong to the specified memory space and ...
a) the address range must not cross the parent PMD address range boundary
b) the PTEs must belong to the same parent PMD

I think b) is good, maybe "PMD entry" to be even clearer.  My previous suggestion of talking about "PTE page" isn't ideal since some architectures have page tables smaller (or larger) than a page.

In other code (e.g., get_and_clear_ptes()) we state "The PTEs are all in the same PMD."

Signature · 3 lines
Cheers,

David
maintainer Acked-by on v2 1/6
#6 lore raw

Re: [PATCH v2 1/6] mm: Make lazy MMU mode context-aware

In reply to Alexander Gordeev
Quoted 14 lines
include/linux/pgtable.h +14 -0
+/**
+ * lazy_mmu_mode_enable_for_pte_range() - Enable the lazy MMU mode with a speedup hint.
+ * @mm: Address space the pages are mapped into.
+ * @addr: Start address of the range.
+ * @end: End address of the range.
+ * @ptep: Page table pointer for the first entry.
+ *
+ * Enters a new lazy MMU mode section; if the mode was not already enabled,
+ * enables it and calls arch_enter_lazy_mmu_mode_for_pte_range().
+ *
+ * PTEs that fall within the specified range might observe update speedups.
+ * The PTE range must belong to the specified memory space and not cross
+ * a page table boundary.
+ *

include/linux/pgtable.h static inline void lazy_mmu_mode_enable(void) · line 305

Quoted 10 lines
include/linux/pgtable.h +10 -0
+ * There are no requirements on the order or range completeness of PTE
+ * updates for the specified range.
+ *
+ * Must be paired with a call to lazy_mmu_mode_disable().
+ *
+ * Has no effect if called:
+ * - While paused - see lazy_mmu_mode_pause()
+ * - In interrupt context
+ */
+static inline void lazy_mmu_mode_enable_for_pte_range(struct mm_struct *mm,

I'm still wondering about a better name for this. "enable_for" indeed sounds like it's only for that range.

Maybe really something including a hint like you previously proposed

lazy_mmu_mode_enable_with_ptes()

lazy_mmu_mode_enable_with_pte_hints()

I think lazy_mmu_mode_enable_with_ptes() is neat.

Thoughts?

In any case, on the general approach + semantics:

include/linux/pgtable.h static inline void lazy_mmu_mode_enable(void) · line 316

Quoted 11 lines
include/linux/pgtable.h +11 -0
+		unsigned long addr, unsigned long end, pte_t *ptep)
+{
+	struct lazy_mmu_state *state = &current->lazy_mmu_state;
+
+	if (in_interrupt() || state->pause_count > 0)
+		return;
+
+	VM_WARN_ON_ONCE(state->enable_count == U8_MAX);
+
+	if (state->enable_count++ == 0)
+		arch_enter_lazy_mmu_mode_for_pte_range(mm, addr, end, ptep);

Okay, so the "hint" is ignored for nesting levels, that works.

Signature · 3 lines
Cheers,

David
author maintainer
#7 lore raw

Re: [PATCH v2 1/6] mm: Make lazy MMU mode context-aware

Quoted 5 lines · David Hildenbrand (Arm)

I'm still wondering about a better name for this. "enable_for" indeed sounds like it's only for that range.

Maybe really something including a hint like you previously proposed

Quoted 7 lines

lazy_mmu_mode_enable_with_ptes()

lazy_mmu_mode_enable_with_pte_hints()

I think lazy_mmu_mode_enable_with_ptes() is neat.

Thoughts?

Works for me.

Quoted 3 lines

In any case, on the general approach + semantics:

...

Quoted 4 lines
Signature · 3 lines
Cheers,

David

Thanks!

maintainer
#8 lore raw

Re: [PATCH v2 1/6] mm: Make lazy MMU mode context-aware

In reply to Alexander Gordeev
Quoted 26 lines · Alexander Gordeev

Lazy MMU mode is assumed to be context-independent, in the sense that it does not need any additional information while operating. However, the s390 architecture benefits from knowing the exact page table entries being modified.

Introduce lazy_mmu_mode_enable_for_pte_range(), which is provided with the process address space and the page table being operated on. This information is required to enable s390-specific optimizations.

The function takes parameters that are typically passed to page- table level walkers, which implies that the span of PTE entries never crosses a page table boundary.

Architectures that do not require such information simply do not need to define the lazy_mmu_mode_enable_for_pte_range() callback.

---
 fs/proc/task_mmu.c      |  2 +-
 include/linux/pgtable.h | 47 +++++++++++++++++++++++++++++++++++++++++
 mm/madvise.c            |  8 +++----
 mm/memory.c             |  8 +++----
 mm/mprotect.c           |  2 +-
 mm/mremap.c             |  2 +-
 mm/vmalloc.c            |  6 +++---
 7 files changed, 61 insertions(+), 14 deletions(-)

...

Quoted 4 lines

+/**
+ * lazy_mmu_mode_enable_for_pte_range() - Enable the lazy MMU mode with a speedup hint.
+ * @mm: Address space the pages are mapped into.
+ * @addr: Start address of the range.

Quoted 8 lines

+ * @end: End address of the range.
+ * @ptep: Page table pointer for the first entry.
+ *
+ * Enters a new lazy MMU mode section; if the mode was not already enabled,
+ * enables it and calls arch_enter_lazy_mmu_mode_for_pte_range().
+ *
+ * PTEs that fall within the specified range might observe update speedups.
+ * The PTE range must belong to the specified memory space and not cross

I guess it would be better use consistent terminology: "memory space" should be changed to "address space" in order to avoid confusion.

author maintainer
#9 lore raw

PATCH v2 2/6 mm/pgtable: Fix bogus comment to clear_not_present_full_ptes()

In reply to Alexander Gordeev

The address provided to clear_not_present_full_ptes() is the address of the underlying memory, not address of the first PTE. The exact wording is taken from clear_ptes() comment.

---
 include/linux/pgtable.h | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
include/linux/pgtable.h +2 -2
index 9ff7b78d65b1..2b82a71f13d7 100644
@@ -1021,8 +1021,8 @@ static inline void pte_clear_not_present_full(struct mm_struct *mm,
 /**
  * clear_not_present_full_ptes - Clear multiple not present PTEs which are
  *				 consecutive in the pgtable.
- * @mm: Address space the ptes represent.
- * @addr: Address of the first pte.
+ * @mm: Address space the pages are mapped into.
+ * @addr: Address the first page is mapped at.
  * @ptep: Page table pointer for the first entry.
  * @nr: Number of entries to clear.
  * @full: Whether we are clearing a full mm.
Signature · 1 line
2.51.0
#10 lore raw

Re: [PATCH v2 2/6] mm/pgtable: Fix bogus comment to clear_not_present_full_ptes()

Quoted 15 lines · Alexander Gordeev

The address provided to clear_not_present_full_ptes() is the address of the underlying memory, not address of the first PTE. The exact wording is taken from clear_ptes() comment.

---
 include/linux/pgtable.h | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
include/linux/pgtable.h +0 -0
index 9ff7b78d65b1..2b82a71f13d7 100644

include/linux/pgtable.h static inline void pte_clear_not_present_full(struct mm_struct *mm, · line 1025

Quoted 7 lines
include/linux/pgtable.h +2 -2
@@ -1021,8 +1021,8 @@ static inline void pte_clear_not_present_full(struct mm_struct *mm,
 /**
  * clear_not_present_full_ptes - Clear multiple not present PTEs which are
  *				 consecutive in the pgtable.
- * @mm: Address space the ptes represent.
- * @addr: Address of the first pte.
+ * @mm: Address space the pages are mapped into.
+ * @addr: Address the first page is mapped at.

Talking about pages with non-present entries does not make sense. So both changes are wrong. Can you find something better and send that out separately? I don't think this is required for your series, right?

Also, looking at it, it does not make sense to specify "full" when there is nothing to flush. We are dealing with non-present PTEs after all.

Looking into the sparc variant of pte_clear_not_present_full(), it does

	__set_pte_at((mm), (addr), (ptep), __pte(0UL), (fullmm))

and that does

	pte_t orig = *ptep;

	*ptep = pte;
	maybe_tlb_batch_add(mm, addr, ptep, orig, fullmm, PAGE_SHIFT);

but in maybe_tlb_batch_add we have

	if (likely(mm != &init_mm) && pte_accessible(mm, orig))
		tlb_batch_add(mm, vaddr, ptep, orig, fullmm, hugepage_shift);

And as the pte is non-present, the pte is certainly not accessible. fullmm is unused on that path.

All the talk about flushing TLBs about something that is non-present for pte_clear_not_present_full looks weird.

@Ryan, you added clear_not_present_full_ptes() back then, but you were simply reusing pte_clear_not_present_full(). Does below seem reasonable to you?

From 0899495ba8ecc73273450e58737bc458976dfa12 Mon Sep 17 00:00:00 2001
From: "David Hildenbrand (Arm)" <[email protected]>
Date: Thu, 16 Apr 2026 09:51:07 +0200
Subject: [PATCH] tmp

---
 arch/sparc/include/asm/pgtable_64.h |  4 ----
 include/linux/pgtable.h             | 28 +++++-----------------------
 mm/madvise.c                        |  6 +++---
 mm/memory.c                         |  2 +-
 4 files changed, 9 insertions(+), 31 deletions(-)
arch/sparc/include/asm/pgtable_64.h +0 -4
index 74ede706fb32..0837ebbc5dce 100644
@@ -945,10 +945,6 @@ static inline void set_ptes(struct mm_struct *mm, unsigned long addr,
 #define pte_clear(mm,addr,ptep)		\
 	set_pte_at((mm), (addr), (ptep), __pte(0UL))
 
-#define __HAVE_ARCH_PTE_CLEAR_NOT_PRESENT_FULL
-#define pte_clear_not_present_full(mm,addr,ptep,fullmm)	\
-	__set_pte_at((mm), (addr), (ptep), __pte(0UL), (fullmm))
-
 #ifdef DCACHE_ALIASING_POSSIBLE
 #define __HAVE_ARCH_MOVE_PTE
 #define move_pte(pte, old_addr, new_addr)				\
include/linux/pgtable.h +5 -23
index 17d961c612fc..b3d6c8ddd687 100644
@@ -955,24 +955,8 @@ static inline void update_mmu_tlb(struct vm_area_struct *vma,
 	update_mmu_tlb_range(vma, address, ptep, 1);
 }
 
-/*
- * Some architectures may be able to avoid expensive synchronization
- * primitives when modifications are made to PTE's which are already
- * not present, or in the process of an address space destruction.
- */
-#ifndef __HAVE_ARCH_PTE_CLEAR_NOT_PRESENT_FULL
-static inline void pte_clear_not_present_full(struct mm_struct *mm,
-					      unsigned long address,
-					      pte_t *ptep,
-					      int full)
-{
-	pte_clear(mm, address, ptep);
-}
-#endif
-
-#ifndef clear_not_present_full_ptes
 /**
- * clear_not_present_full_ptes - Clear multiple not present PTEs which are
+ * clear_not_present_full_ptes - Clear multiple non-present PTEs which are
  *				 consecutive in the pgtable.
  * @mm: Address space the ptes represent.
  * @addr: Address of the first pte.
@@ -980,24 +964,22 @@ static inline void pte_clear_not_present_full(struct mm_struct *mm,
  * @nr: Number of entries to clear.
  * @full: Whether we are clearing a full mm.
  *
- * May be overridden by the architecture; otherwise, implemented as a simple
- * loop over pte_clear_not_present_full().
+ * Implemented as a simple loop over pte_clear().
  *
  * Context: The caller holds the page table lock.  The PTEs are all not present.
  * The PTEs are all in the same PMD.
  */
-static inline void clear_not_present_full_ptes(struct mm_struct *mm,
-		unsigned long addr, pte_t *ptep, unsigned int nr, int full)
+static inline void clear_non_present_ptes(struct mm_struct *mm,
+		unsigned long addr, pte_t *ptep, unsigned int nr)
 {
 	for (;;) {
-		pte_clear_not_present_full(mm, addr, ptep, full);
+		pte_clear(mm, addr, ptep);
 		if (--nr == 0)
 			break;
 		ptep++;
 		addr += PAGE_SIZE;
 	}
 }
-#endif
 
 #ifndef __HAVE_ARCH_PTEP_CLEAR_FLUSH
 extern pte_t ptep_clear_flush(struct vm_area_struct *vma,
mm/madvise.c +3 -3
index 69708e953cf5..0e430e03e69c 100644
@@ -695,10 +695,10 @@ static int madvise_free_pte_range(pmd_t *pmd, unsigned long addr,
 				nr = swap_pte_batch(pte, max_nr, ptent);
 				nr_swap -= nr;
 				swap_put_entries_direct(entry, nr);
-				clear_not_present_full_ptes(mm, addr, pte, nr, tlb->fullmm);
+				clear_non_present_(mm, addr, pte, nr);
 			} else if (softleaf_is_hwpoison(entry) ||
 				   softleaf_is_poison_marker(entry)) {
-				pte_clear_not_present_full(mm, addr, pte, tlb->fullmm);
+				pte_clear(mm, addr, pte);
 			}
 			continue;
 		}
@@ -1234,7 +1234,7 @@ static int guard_remove_pte_entry(pte_t *pte, unsigned long addr,
 
 	if (is_guard_pte_marker(ptent)) {
 		/* Simply clear the PTE marker. */
-		pte_clear_not_present_full(walk->mm, addr, pte, false);
+		pte_clear(walk->mm, addr, pte);
 		update_mmu_cache(walk->vma, addr, pte);
 	}
 
mm/memory.c +1 -1
index 631205a384e1..d61d4ba906cd 100644
@@ -1781,7 +1781,7 @@ static inline int zap_nonpresent_ptes(struct mmu_gather *tlb,
 		pr_alert("unrecognized swap entry 0x%lx\n", entry.val);
 		WARN_ON_ONCE(1);
 	}
-	clear_not_present_full_ptes(vma->vm_mm, addr, pte, nr, tlb->fullmm);
+	clear_non_present_ptes(vma->vm_mm, addr, pte, nr);
 	*any_skipped = zap_install_uffd_wp_if_needed(vma, addr, pte, nr, details, ptent);
 
 	return nr;
Signature · 10 lines
2.43.0





-- 
Cheers,

David
author maintainer
#11 lore raw

Re: [PATCH v2 2/6] mm/pgtable: Fix bogus comment to clear_not_present_full_ptes()

Quoted 3 lines · David Hildenbrand (Arm)

Talking about pages with non-present entries does not make sense. So both changes are wrong. Can you find something better and send that out separately? I don't think this is required for your series, right?

Right.

author maintainer
#12 lore raw

PATCH v2 3/6 s390/mm: Complete ptep_get() conversion

In reply to Alexander Gordeev

Finalize commit c33c794828f2 ("mm: ptep_get() conversion") and replace direct page table entry dereferencing with the proper accessors (ptep_get(), pmdp_get(), etc.).

Override the default getter implementations even though they are currently identical: pud_clear(), p4d_clear(), and pgd_clear() require corresponding architecture-specific getters, but these are not yet defined. This avoids a dependency loop.

---
 arch/s390/boot/vmem.c           | 32 +++++++------
 arch/s390/include/asm/hugetlb.h |  2 +-
 arch/s390/include/asm/pgtable.h | 60 ++++++++++++++++++------
 arch/s390/mm/hugetlbpage.c      | 12 ++---
 arch/s390/mm/pageattr.c         | 42 +++++++++--------
 arch/s390/mm/vmem.c             | 82 ++++++++++++++++++---------------
 6 files changed, 138 insertions(+), 92 deletions(-)
arch/s390/boot/vmem.c +17 -15
index 7d6cc4c85af0..ff6d58a476ba 100644
@@ -338,7 +338,7 @@ static void pgtable_pte_populate(pmd_t *pmd, unsigned long addr, unsigned long e
 
 	pte = pte_offset_kernel(pmd, addr);
 	for (; addr < end; addr += PAGE_SIZE, pte++) {
-		if (pte_none(*pte)) {
+		if (pte_none(ptep_get(pte))) {
 			if (kasan_pte_populate_zero_shadow(pte, mode))
 				continue;
 			entry = __pte(resolve_pa_may_alloc(addr, PAGE_SIZE, mode));
@@ -355,26 +355,27 @@ static void pgtable_pmd_populate(pud_t *pud, unsigned long addr, unsigned long e
 				 enum populate_mode mode)
 {
 	unsigned long pa, next, pages = 0;
-	pmd_t *pmd, entry;
+	pmd_t *pmd, entry, large_entry;
 	pte_t *pte;
 
 	pmd = pmd_offset(pud, addr);
 	for (; addr < end; addr = next, pmd++) {
 		next = pmd_addr_end(addr, end);
-		if (pmd_none(*pmd)) {
+		entry = pmdp_get(pmd);
+		if (pmd_none(entry)) {
 			if (kasan_pmd_populate_zero_shadow(pmd, addr, next, mode))
 				continue;
 			pa = try_get_large_pmd_pa(pmd, addr, next, mode);
 			if (pa != INVALID_PHYS_ADDR) {
-				entry = __pmd(pa);
-				entry = set_pmd_bit(entry, SEGMENT_KERNEL);
-				set_pmd(pmd, entry);
+				large_entry = __pmd(pa);
+				large_entry = set_pmd_bit(large_entry, SEGMENT_KERNEL);
+				set_pmd(pmd, large_entry);
 				pages++;
 				continue;
 			}
 			pte = boot_pte_alloc();
 			pmd_populate(&init_mm, pmd, pte);
-		} else if (pmd_leaf(*pmd)) {
+		} else if (pmd_leaf(entry)) {
 			continue;
 		}
 		pgtable_pte_populate(pmd, addr, next, mode);
@@ -387,26 +388,27 @@ static void pgtable_pud_populate(p4d_t *p4d, unsigned long addr, unsigned long e
 				 enum populate_mode mode)
 {
 	unsigned long pa, next, pages = 0;
-	pud_t *pud, entry;
+	pud_t *pud, entry, large_entry;
 	pmd_t *pmd;
 
 	pud = pud_offset(p4d, addr);
 	for (; addr < end; addr = next, pud++) {
 		next = pud_addr_end(addr, end);
-		if (pud_none(*pud)) {
+		entry = pudp_get(pud);
+		if (pud_none(entry)) {
 			if (kasan_pud_populate_zero_shadow(pud, addr, next, mode))
 				continue;
 			pa = try_get_large_pud_pa(pud, addr, next, mode);
 			if (pa != INVALID_PHYS_ADDR) {
-				entry = __pud(pa);
-				entry = set_pud_bit(entry, REGION3_KERNEL);
-				set_pud(pud, entry);
+				large_entry = __pud(pa);
+				large_entry = set_pud_bit(large_entry, REGION3_KERNEL);
+				set_pud(pud, large_entry);
 				pages++;
 				continue;
 			}
 			pmd = boot_crst_alloc(_SEGMENT_ENTRY_EMPTY);
 			pud_populate(&init_mm, pud, pmd);
-		} else if (pud_leaf(*pud)) {
+		} else if (pud_leaf(entry)) {
 			continue;
 		}
 		pgtable_pmd_populate(pud, addr, next, mode);
@@ -425,7 +427,7 @@ static void pgtable_p4d_populate(pgd_t *pgd, unsigned long addr, unsigned long e
 	p4d = p4d_offset(pgd, addr);
 	for (; addr < end; addr = next, p4d++) {
 		next = p4d_addr_end(addr, end);
-		if (p4d_none(*p4d)) {
+		if (p4d_none(p4dp_get(p4d))) {
 			if (kasan_p4d_populate_zero_shadow(p4d, addr, next, mode))
 				continue;
 			pud = boot_crst_alloc(_REGION3_ENTRY_EMPTY);
@@ -451,7 +453,7 @@ static void pgtable_populate(unsigned long addr, unsigned long end, enum populat
 	pgd = pgd_offset(&init_mm, addr);
 	for (; addr < end; addr = next, pgd++) {
 		next = pgd_addr_end(addr, end);
-		if (pgd_none(*pgd)) {
+		if (pgd_none(pgdp_get(pgd))) {
 			if (kasan_pgd_populate_zero_shadow(pgd, addr, next, mode))
 				continue;
 			p4d = boot_crst_alloc(_REGION2_ENTRY_EMPTY);
arch/s390/include/asm/hugetlb.h +1 -1
index 6983e52eaf81..e33a5b587ee4 100644
@@ -41,7 +41,7 @@ static inline pte_t huge_ptep_get_and_clear(struct mm_struct *mm,
 static inline void huge_pte_clear(struct mm_struct *mm, unsigned long addr,
 				  pte_t *ptep, unsigned long sz)
 {
-	if ((pte_val(*ptep) & _REGION_ENTRY_TYPE_MASK) == _REGION_ENTRY_TYPE_R3)
+	if ((pte_val(ptep_get(ptep)) & _REGION_ENTRY_TYPE_MASK) == _REGION_ENTRY_TYPE_R3)
 		set_pte(ptep, __pte(_REGION3_ENTRY_EMPTY));
 	else
 		set_pte(ptep, __pte(_SEGMENT_ENTRY_EMPTY));
arch/s390/include/asm/pgtable.h +45 -15

104 lines not on this page · raw message

arch/s390/mm/hugetlbpage.c +6 -6
index 302ef5781b65..db35d8fe8609 100644
@@ -143,7 +143,7 @@ void __set_huge_pte_at(struct mm_struct *mm, unsigned long addr,
 	rste = __pte_to_rste(pte);
 
 	/* Set correct table type for 2G hugepages */
-	if ((pte_val(*ptep) & _REGION_ENTRY_TYPE_MASK) == _REGION_ENTRY_TYPE_R3) {
+	if ((pte_val(ptep_get(ptep)) & _REGION_ENTRY_TYPE_MASK) == _REGION_ENTRY_TYPE_R3) {
 		if (likely(pte_present(pte)))
 			rste |= _REGION3_ENTRY_LARGE;
 		rste |= _REGION_ENTRY_TYPE_R3;
@@ -161,7 +161,7 @@ void set_huge_pte_at(struct mm_struct *mm, unsigned long addr,
 
 pte_t huge_ptep_get(struct mm_struct *mm, unsigned long addr, pte_t *ptep)
 {
-	return __rste_to_pte(pte_val(*ptep));
+	return __rste_to_pte(pte_val(ptep_get(ptep)));
 }
 
 pte_t __huge_ptep_get_and_clear(struct mm_struct *mm,
@@ -171,7 +171,7 @@ pte_t __huge_ptep_get_and_clear(struct mm_struct *mm,
 	pmd_t *pmdp = (pmd_t *) ptep;
 	pud_t *pudp = (pud_t *) ptep;
 
-	if ((pte_val(*ptep) & _REGION_ENTRY_TYPE_MASK) == _REGION_ENTRY_TYPE_R3)
+	if ((pte_val(ptep_get(ptep)) & _REGION_ENTRY_TYPE_MASK) == _REGION_ENTRY_TYPE_R3)
 		pudp_xchg_direct(mm, addr, pudp, __pud(_REGION3_ENTRY_EMPTY));
 	else
 		pmdp_xchg_direct(mm, addr, pmdp, __pmd(_SEGMENT_ENTRY_EMPTY));
@@ -209,13 +209,13 @@ pte_t *huge_pte_offset(struct mm_struct *mm,
 	pmd_t *pmdp = NULL;
 
 	pgdp = pgd_offset(mm, addr);
-	if (pgd_present(*pgdp)) {
+	if (pgd_present(pgdp_get(pgdp))) {
 		p4dp = p4d_offset(pgdp, addr);
-		if (p4d_present(*p4dp)) {
+		if (p4d_present(p4dp_get(p4dp))) {
 			pudp = pud_offset(p4dp, addr);
 			if (sz == PUD_SIZE)
 				return (pte_t *)pudp;
-			if (pud_present(*pudp))
+			if (pud_present(pudp_get(pudp)))
 				pmdp = pmd_offset(pudp, addr);
 		}
 	}
arch/s390/mm/pageattr.c +23 -19

120 lines not on this page · raw message

arch/s390/mm/vmem.c +46 -36

253 lines not on this page · raw message

Signature · 1 line
2.51.0
maintainer Acked-by on v2 3/6
#13 lore raw

Re: [PATCH v2 3/6] s390/mm: Complete ptep_get() conversion

Quoted 10 lines · Alexander Gordeev

Finalize commit c33c794828f2 ("mm: ptep_get() conversion") and replace direct page table entry dereferencing with the proper accessors (ptep_get(), pmdp_get(), etc.).

Override the default getter implementations even though they are currently identical: pud_clear(), p4d_clear(), and pgd_clear() require corresponding architecture-specific getters, but these are not yet defined. This avoids a dependency loop.

Quoted 8 lines
---
 arch/s390/boot/vmem.c           | 32 +++++++------
 arch/s390/include/asm/hugetlb.h |  2 +-
 arch/s390/include/asm/pgtable.h | 60 ++++++++++++++++++------
 arch/s390/mm/hugetlbpage.c      | 12 ++---
 arch/s390/mm/pageattr.c         | 42 +++++++++--------
 arch/s390/mm/vmem.c             | 82 ++++++++++++++++++---------------
 6 files changed, 138 insertions(+), 92 deletions(-)

Was this a manual conversion, or did you use some tooling?

I'm wondering since I can spot not converted locations, e.g. one in __kernel_map_pages().

Besides that:

author maintainer
#14 lore raw

Re: [PATCH v2 3/6] s390/mm: Complete ptep_get() conversion

Quoted 1 line · Heiko Carstens

Was this a manual conversion, or did you use some tooling?

Manual :) But I will try to recheck with a tool.

Quoted 2 lines

I'm wondering since I can spot not converted locations, e.g. one in __kernel_map_pages().

Whether this fixup is fine?

arch/s390/mm/pageattr.c +2 -1
index 3a54860cb05f..e6f788696dd1 100644
@@ -455,7 +455,8 @@ void __kernel_map_pages(struct page *page, int numpages, int enable)
 		nr = min(numpages - i, nr);
 		if (enable) {
 			for (j = 0; j < nr; j++) {
-				pte = clear_pte_bit(*ptep, __pgprot(_PAGE_INVALID));
+				pte = ptep_get(ptep);
+				pte = clear_pte_bit(pte, __pgprot(_PAGE_INVALID));
 				set_pte(ptep, pte);
 				address += PAGE_SIZE;
 				ptep++;
Quoted 2 lines

Besides that:

maintainer
#15 lore raw

Re: [PATCH v2 3/6] s390/mm: Complete ptep_get() conversion

Quoted 15 lines · Alexander Gordeev
Quoted 1 line · Heiko Carstens

Was this a manual conversion, or did you use some tooling?

Manual :) But I will try to recheck with a tool.

Quoted 2 lines

I'm wondering since I can spot not converted locations, e.g. one in __kernel_map_pages().

Whether this fixup is fine?

arch/s390/mm/pageattr.c +0 -0
index 3a54860cb05f..e6f788696dd1 100644

arch/s390/mm/pageattr.c void __kernel_map_pages(struct page *page, int numpages, int enable) · line 459

Quoted 6 lines
arch/s390/mm/pageattr.c +2 -1
@@ -455,7 +455,8 @@ void __kernel_map_pages(struct page *page, int numpages, int enable)
 		nr = min(numpages - i, nr);
 		if (enable) {
 			for (j = 0; j < nr; j++) {
-				pte = clear_pte_bit(*ptep, __pgprot(_PAGE_INVALID));
+				pte = ptep_get(ptep);
+				pte = clear_pte_bit(pte, __pgprot(_PAGE_INVALID));

Sure.

author maintainer
#16 lore raw

PATCH v2 4/6 s390/mm: Make PTC and UV call order consistent

In reply to Alexander Gordeev

In various code paths, page_table_check_pte_clear() is called before converting a secure page, while in others it is called after. Make this consistent and always perform the conversion after the PTC hook has been called. Also make all conversion‑ eligibility condition checks look the same, and rework the one in ptep_get_and_clear_full() slightly.

---
 arch/s390/include/asm/pgtable.h | 39 +++++++++++++++------------------
 1 file changed, 18 insertions(+), 21 deletions(-)
arch/s390/include/asm/pgtable.h +18 -21
index 42688ea4337f..010a33fec867 100644
@@ -1219,10 +1219,10 @@ static inline pte_t ptep_get_and_clear(struct mm_struct *mm,
 	pte_t res;
 
 	res = ptep_xchg_lazy(mm, addr, ptep, __pte(_PAGE_INVALID));
+	page_table_check_pte_clear(mm, addr, res);
 	/* At this point the reference through the mapping is still present */
 	if (mm_is_protected(mm) && pte_present(res))
 		WARN_ON_ONCE(uv_convert_from_secure_pte(res));
-	page_table_check_pte_clear(mm, addr, res);
 	return res;
 }
 
@@ -1238,10 +1238,10 @@ static inline pte_t ptep_clear_flush(struct vm_area_struct *vma,
 	pte_t res;
 
 	res = ptep_xchg_direct(vma->vm_mm, addr, ptep, __pte(_PAGE_INVALID));
+	page_table_check_pte_clear(vma->vm_mm, addr, res);
 	/* At this point the reference through the mapping is still present */
 	if (mm_is_protected(vma->vm_mm) && pte_present(res))
 		WARN_ON_ONCE(uv_convert_from_secure_pte(res));
-	page_table_check_pte_clear(vma->vm_mm, addr, res);
 	return res;
 }
 
@@ -1265,26 +1265,23 @@ static inline pte_t ptep_get_and_clear_full(struct mm_struct *mm,
 	} else {
 		res = ptep_xchg_lazy(mm, addr, ptep, __pte(_PAGE_INVALID));
 	}
-
 	page_table_check_pte_clear(mm, addr, res);
-
-	/* Nothing to do */
-	if (!mm_is_protected(mm) || !pte_present(res))
-		return res;
-	/*
-	 * At this point the reference through the mapping is still present.
-	 * The notifier should have destroyed all protected vCPUs at this
-	 * point, so the destroy should be successful.
-	 */
-	if (full && !uv_destroy_pte(res))
-		return res;
-	/*
-	 * If something went wrong and the page could not be destroyed, or
-	 * if this is not a mm teardown, the slower export is used as
-	 * fallback instead. If even that fails, print a warning and leak
-	 * the page, to avoid crashing the whole system.
-	 */
-	WARN_ON_ONCE(uv_convert_from_secure_pte(res));
+	/* At this point the reference through the mapping is still present */
+	if (mm_is_protected(mm) && pte_present(res)) {
+		/*
+		 * The notifier should have destroyed all protected vCPUs at
+		 * this point, so the destroy should be successful.
+		 */
+		if (full && !uv_destroy_pte(res))
+			return res;
+		/*
+		 * If something went wrong and the page could not be destroyed,
+		 * or if this is not a mm teardown, the slower export is used
+		 * as fallback instead. If even that fails, print a warning and
+		 * leak the page, to avoid crashing the whole system.
+		 */
+		WARN_ON_ONCE(uv_convert_from_secure_pte(res));
+	}
 	return res;
 }
 
Signature · 1 line
2.51.0
maintainer
#17 lore raw

Re: [PATCH v2 4/6] s390/mm: Make PTC and UV call order consistent

Quoted 7 lines · Alexander Gordeev

In various code paths, page_table_check_pte_clear() is called before converting a secure page, while in others it is called after. Make this consistent and always perform the conversion after the PTC hook has been called. Also make all conversion‑ eligibility condition checks look the same, and rework the one in ptep_get_and_clear_full() slightly.

Quoted 5 lines
---
 arch/s390/include/asm/pgtable.h | 39 +++++++++++++++------------------
 1 file changed, 18 insertions(+), 21 deletions(-)

Why is this required for this series? It is anything but obvious to me. If it is required please add some reasoning e.g. in the cover letter or to the above changelog.

author maintainer
#18 lore raw

Re: [PATCH v2 4/6] s390/mm: Make PTC and UV call order consistent

Quoted 14 lines · Heiko Carstens
Quoted 7 lines · Alexander Gordeev

In various code paths, page_table_check_pte_clear() is called before converting a secure page, while in others it is called after. Make this consistent and always perform the conversion after the PTC hook has been called. Also make all conversion‑ eligibility condition checks look the same, and rework the one in ptep_get_and_clear_full() slightly.

Quoted 5 lines
---
 arch/s390/include/asm/pgtable.h | 39 +++++++++++++++------------------
 1 file changed, 18 insertions(+), 21 deletions(-)
Quoted 3 lines

Why is this required for this series? It is anything but obvious to me. If it is required please add some reasoning e.g. in the cover letter or to the above changelog.

This is because changes to ptep_get_and_clear() and ptep_clear_flush() look much more clean with this patch. Otherwise, I would have to move page_table_check_pte_clear() before the UV check, which is strictly speaking worth a separate patch - this patch.

It also makes sense as a cleanup. I would prefer to see it as part of the PTC series, but at the time I did not realize it was worthwhile.

author maintainer
#19 lore raw

PATCH v2 5/6 s390/mm: Batch PTE updates in lazy MMU mode

In reply to Alexander Gordeev

Make use of the IPTE instruction's "Additional Entries" field to invalidate multiple PTEs in one go while in lazy MMU mode. This is the mode in which many memory-management system calls (like mremap(), mprotect(), etc.) update memory attributes.

To achieve that, the set_pte() and ptep_get() primitives use a per-CPU cache to store and retrieve PTE values and apply the cached values to the real page table once lazy MMU mode is left.

The same is done for memory-management platform callbacks that would otherwise cause intense per-PTE IPTE traffic, reducing the number of IPTE instructions from up to PTRS_PER_PTE to a single instruction in the best case. The average reduction is of course smaller.

Since all existing page table iterators called in lazy MMU mode handle one table at a time, the per-CPU cache does not need to be larger than PTRS_PER_PTE entries. That also naturally aligns with the IPTE instruction, which must not cross a page table boundary.

Before this change, the system calls did:

	lazy_mmu_mode_enable_pte()
	...
	<update PTEs>		// up to PTRS_PER_PTE single-IPTEs
	...
	lazy_mmu_mode_disable()

With this change, the system calls do:

lazy_mmu_mode_enable_pte()
...
<store new PTE values in the per-CPU cache>
...
lazy_mmu_mode_disable() // apply cache with one multi-IPTE

When applied to large memory ranges, some system calls show significant speedups:

    mprotect()    ~15x
    munmap()      ~3x
    mremap()      ~28x

At the same time, fork() shows a measurable slowdown of ~1.5x.

The overall results depend on memory size and access patterns, but the change generally does not degrade performance.

In addition to a process-wide impact, the rework affects the whole Central Electronics Complex (CEC). Each (global) IPTE instruction initiates a quiesce state in a CEC, so reducing the number of IPTE calls relieves CEC-wide quiesce traffic.

In an extreme case of mprotect() contiguously triggering the quiesce state on four LPARs in parallel, measurements show ~25x fewer quiesce events.

Assisted-by gemini:gemini-3.1-pro-preview
---
 arch/s390/Kconfig               |  11 +
 arch/s390/include/asm/pgtable.h | 169 ++++++++++++--
 arch/s390/mm/Makefile           |   1 +
 arch/s390/mm/ipte_batch.c       | 378 ++++++++++++++++++++++++++++++++
 arch/s390/mm/pgtable.c          |   8 +-
 5 files changed, 545 insertions(+), 22 deletions(-)
 create mode 100644 arch/s390/mm/ipte_batch.c
arch/s390/Kconfig +11 -0
index 7828fbe0fc42..deffc819268e 100644
@@ -732,6 +732,17 @@ config MAX_PHYSMEM_BITS
 	  Increasing the number of bits also increases the kernel image size.
 	  By default 46 bits (64TB) are supported.
 
+config IPTE_BATCH
+	def_bool y
+	prompt "Enables Additional Entries for IPTE instruction"
+	select ARCH_HAS_LAZY_MMU_MODE
+	help
+	  This option enables using of "Additional Entries" field of the IPTE
+	  instruction, which capitalizes on the lazy MMU mode infrastructure.
+	  As result, multiple PTEs are invalidated in one go. That improves
+	  performance of many memory-management system calls (like mremap(),
+	  mprotect(), etc.) and decreases CEC-wide quiesce traffic.
+
 endmenu
 
 menu "I/O subsystem"
arch/s390/include/asm/pgtable.h +151 -18

232 lines not on this page · raw message

arch/s390/mm/Makefile +1 -0
index 193899c39ca7..0f6c6de447d4 100644
@@ -11,5 +11,6 @@ obj-$(CONFIG_DEBUG_VIRTUAL)	+= physaddr.o
 obj-$(CONFIG_HUGETLB_PAGE)	+= hugetlbpage.o
 obj-$(CONFIG_PTDUMP)		+= dump_pagetables.o
 obj-$(CONFIG_PFAULT)		+= pfault.o
+obj-$(CONFIG_IPTE_BATCH)	+= ipte_batch.o
 
 obj-$(subst m,y,$(CONFIG_KVM))	+= gmap_helpers.o
arch/s390/mm/ipte_batch.c new file +378 -0

378 lines not on this page · raw message

arch/s390/mm/pgtable.c +4 -4
index 4acd8b140c4b..df36523bcbbb 100644
@@ -166,14 +166,14 @@ pte_t ptep_xchg_lazy(struct mm_struct *mm, unsigned long addr,
 }
 EXPORT_SYMBOL(ptep_xchg_lazy);
 
-pte_t ptep_modify_prot_start(struct vm_area_struct *vma, unsigned long addr,
-			     pte_t *ptep)
+pte_t ___ptep_modify_prot_start(struct vm_area_struct *vma, unsigned long addr,
+				pte_t *ptep)
 {
 	return ptep_flush_lazy(vma->vm_mm, addr, ptep, 1);
 }
 
-void ptep_modify_prot_commit(struct vm_area_struct *vma, unsigned long addr,
-			     pte_t *ptep, pte_t old_pte, pte_t pte)
+void ___ptep_modify_prot_commit(struct vm_area_struct *vma, unsigned long addr,
+				pte_t *ptep, pte_t old_pte, pte_t pte)
 {
 	set_pte(ptep, pte);
 }
Signature · 1 line
2.51.0
maintainer
#20 lore raw
Quoted 10 lines · Alexander Gordeev
arch/s390/Kconfig +2 -0
index 7828fbe0fc42..deffc819268e 100644
@@ -732,6 +732,17 @@ config MAX_PHYSMEM_BITS
 	  Increasing the number of bits also increases the kernel image size.
 	  By default 46 bits (64TB) are supported.
 
+config IPTE_BATCH
+	def_bool y

arch/s390/Kconfig config MAX_PHYSMEM_BITS · line 746

Quoted 10 lines
arch/s390/Kconfig +9 -0
+	prompt "Enables Additional Entries for IPTE instruction"
+	select ARCH_HAS_LAZY_MMU_MODE
+	help
+	  This option enables using of "Additional Entries" field of the IPTE
+	  instruction, which capitalizes on the lazy MMU mode infrastructure.
+	  As result, multiple PTEs are invalidated in one go. That improves
+	  performance of many memory-management system calls (like mremap(),
+	  mprotect(), etc.) and decreases CEC-wide quiesce traffic.
+
 endmenu

Just some general comments before I find time to look deeper into this
series: please let us not add yet another config option. This will be enabled anywhere in any case.

Not having this option makes maintenance easier.

arch/s390/Kconfig config MAX_PHYSMEM_BITS

Quoted 8 lines
arch/s390/Kconfig +8 -0
+#if !defined(CONFIG_IPTE_BATCH) || defined(__DECOMPRESSOR)
+static inline
+bool ipte_batch_ptep_test_and_clear_young(struct vm_area_struct *vma,
+					  unsigned long addr, pte_t *ptep,
+					  int *res)
+{
+	return false;
+}

Why does this depend on __DECOMPRESSOR? Which code in the decompressor would generate a call to the ipte_batch functions?

author maintainer
#21 lore raw
Quoted 11 lines · Heiko Carstens

arch/s390/include/asm/pgtable.h enum { · line 49

Quoted 8 lines
arch/s390/include/asm/pgtable.h +8 -0
+#if !defined(CONFIG_IPTE_BATCH) || defined(__DECOMPRESSOR)
+static inline
+bool ipte_batch_ptep_test_and_clear_young(struct vm_area_struct *vma,
+					  unsigned long addr, pte_t *ptep,
+					  int *res)
+{
+	return false;
+}

Why does this depend on __DECOMPRESSOR? Which code in the decompressor would generate a call to the ipte_batch functions?

Any call to ptep_get() or set_pte().

maintainer
#22 lore raw
In reply to Alexander Gordeev
Quoted 28 lines · Alexander Gordeev

Make use of the IPTE instruction's "Additional Entries" field to invalidate multiple PTEs in one go while in lazy MMU mode. This is the mode in which many memory-management system calls (like mremap(), mprotect(), etc.) update memory attributes.

To achieve that, the set_pte() and ptep_get() primitives use a per-CPU cache to store and retrieve PTE values and apply the cached values to the real page table once lazy MMU mode is left.

The same is done for memory-management platform callbacks that would otherwise cause intense per-PTE IPTE traffic, reducing the number of IPTE instructions from up to PTRS_PER_PTE to a single instruction in the best case. The average reduction is of course smaller.

Since all existing page table iterators called in lazy MMU mode handle one table at a time, the per-CPU cache does not need to be larger than PTRS_PER_PTE entries. That also naturally aligns with the IPTE instruction, which must not cross a page table boundary.

Before this change, the system calls did:

	lazy_mmu_mode_enable_pte()
	...
	<update PTEs>		// up to PTRS_PER_PTE single-IPTEs
	...
	lazy_mmu_mode_disable()
Quoted 7 lines

With this change, the system calls do:

lazy_mmu_mode_enable_pte()
...
<store new PTE values in the per-CPU cache>
...
lazy_mmu_mode_disable() // apply cache with one multi-IPTE

I think what is not necessarily immediately obvious: this approach must assure that within such a lazy mmu section there is not a single occurrence of code which doesn't use the above mentioned modified primitives to dereference page table entry pointers.

Directly dereferencing such pointers would bypass the cache and lead to incorrect results. Therefore we do need some mechanism which makes sure this cannot happen. Preferebly that would happen at compile time with static code analysis. Alternatively your Kasan implementation would be helpful to find something like that after-the-fact.

However in any case we need something to address this problem.

Quoted 8 lines
arch/s390/include/asm/pgtable.h +8 -0
+#if !defined(CONFIG_IPTE_BATCH) || defined(__DECOMPRESSOR)
+static inline
+bool ipte_batch_ptep_test_and_clear_young(struct vm_area_struct *vma,
+					  unsigned long addr, pte_t *ptep,
+					  int *res)
+{
+	return false;
+}

...

arch/s390/include/asm/pgtable.h enum { · line 92

Quoted 3 lines
arch/s390/include/asm/pgtable.h +3 -0
+bool ipte_batch_ptep_test_and_clear_young(struct vm_area_struct *vma,
+					  unsigned long addr, pte_t *ptep,
+					  int *res);

Could you change this to something like:

bool __ipte_batch_ptep_test_and_clear_young(struct vm_area_struct *vma,

					    unsigned long addr, pte_t *ptep,
					    int *res);

static inline
bool ipte_batch_ptep_test_and_clear_young(struct vm_area_struct *vma,
unsigned long addr, pte_t *ptep, int *res)

{
	if (__is_defined(__DECOMPRESSOR))
		return false;
	return __ipte_batch_ptep_test_and_clear_young(vma, addr, ptep, res);
}

? This avoids the ifdef and makes the code easier to read and change.

arch/s390/include/asm/pgtable.h static inline void set_pmd(pmd_t *pmdp, pmd_t pmd) · line 1055

Quoted 5 lines
arch/s390/include/asm/pgtable.h +5 -0
+static inline void set_pte(pte_t *ptep, pte_t pte)
+{
+	if (!ipte_batch_set_pte(ptep, pte))
+		__set_pte(ptep, pte);
+}

Not sure if you analyzed it, but it looks like this might be the reason why you see the fork() slowdown: every page table operation now comes with a function call, even if is not needed.

I guess it would be helpful to add an early exit to the ipte_batch() inlines. E.g. going back to the example above:

static inline
bool ipte_batch_ptep_test_and_clear_young(struct vm_area_struct *vma,
unsigned long addr, pte_t *ptep, int *res)

{
	if (__is_defined(__DECOMPRESSOR))
		return false;
--->	if (unlikely(!ipte_batch_active()))
--->		return false;
	return __ipte_batch_ptep_test_and_clear_young(vma, addr, ptep, res);
}

Where ipte_batch_active() would be an inline function which simply tests a bit in lowcore.

arch/s390/mm/Makefile obj-$(CONFIG_DEBUG_VIRTUAL) += physaddr.o · line 14

Quoted 9 lines
arch/s390/mm/Makefile +1 -0
index 193899c39ca7..0f6c6de447d4 100644
@@ -11,5 +11,6 @@ obj-$(CONFIG_DEBUG_VIRTUAL)	+= physaddr.o
 obj-$(CONFIG_HUGETLB_PAGE)	+= hugetlbpage.o
 obj-$(CONFIG_PTDUMP)		+= dump_pagetables.o
 obj-$(CONFIG_PFAULT)		+= pfault.o
+obj-$(CONFIG_IPTE_BATCH)	+= ipte_batch.o

As already stated before: let's get rid of the Kconfig option. This will be enabled anyway.

arch/s390/mm/Makefile obj-$(CONFIG_DEBUG_VIRTUAL) += physaddr.o · line 15

Quoted 1 line
arch/s390/mm/Makefile +1 -0
+#define PTE_POISON	0

Is there a reason why the PTE_POISON value is zero? If it can be a different value, I would like to propose a PTE which has bit 11 set (the one which must be zero). If that would ever be transferred to page table, and it would be used, we would see that immediately :)

arch/s390/mm/Makefile obj-$(CONFIG_DEBUG_VIRTUAL) += physaddr.o

Quoted 11 lines
arch/s390/mm/Makefile +11 -0
+struct ipte_batch {
+	struct mm_struct *mm;
+	unsigned long base_addr;
+	unsigned long base_end;
+	pte_t *base_pte;
+	pte_t *start_pte;
+	pte_t *end_pte;
+	pte_t cache[PTRS_PER_PTE];
+};
+
+static DEFINE_PER_CPU(struct ipte_batch, ipte_range);

This is ~1MB with 512 possible CPUs. I think it would make sense to allocate and free this data structure with CPU hotplug to avoid wasting too much memory.

Quoted 10 lines
arch/s390/mm/Makefile +10 -0
+static int count_contiguous(pte_t *start, pte_t *end, bool *valid)
+{
+	pte_t pte = __ptep_get(start);
+	pte_t *ptep;
+
+	*valid = !(pte_val(pte) & _PAGE_INVALID);
+
+	for (ptep = start + 1; ptep < end; ptep++) {
+		pte = __ptep_get(ptep);
+		if (*valid) {

arch/s390/mm/Makefile

Quoted 10 lines
arch/s390/mm/Makefile +10 -0
+			if (pte_val(pte) & _PAGE_INVALID)
+				break;
+		} else {
+			if (!(pte_val(pte) & _PAGE_INVALID))
+				break;
+		}
+	}
+
+	return ptep - start;
+}

Wouldn't it be possible to shorten this a bit? E.g.:

static int count_contiguous(pte_t *start, pte_t *end, bool *valid)

{
	unsigned long inv;
	pte_t *ptep;

	inv = pte_val(__ptep_get(start)) & _PAGE_INVALID;
	*valid = !inv;
	for (ptep = start + 1; ptep < end; ptep++) {
		if (pte_val(__ptep_get(ptep)) & _PAGE_INVALID != inv)
			break;
	}
	return ptep - start;
}

arch/s390/mm/Makefile

Quoted 6 lines
arch/s390/mm/Makefile +6 -0
+static void __invalidate_pte_range(struct mm_struct *mm, unsigned long addr,
+				   int nr_ptes, pte_t *ptep)
+{
+	atomic_inc(&mm->context.flush_count);
+	if (cpu_has_tlb_lc() &&
+	    cpumask_equal(mm_cpumask(mm), cpumask_of(smp_processor_id())))

One line please. Yes, I know, this is more or less copy-pasted, but... :)

Just a general comment about the naming conventions: imho ipte_batch is not a nice choice, since the name of the facility is "ipte range". However I would abstract even more, since nobody knows if there will be a different instruction or facility to achieve all of this in a better way.

Anyway... maybe rename the file simply to mmu.c or tlb.c and change the function prefixes accordingly so we end up with shorter function names?

author maintainer
#23 lore raw

On Thu, Apr 23, 2026 at 02:28:24PM +0200, Heiko Carstens wrote:

Hi Heiko!

...

Quoted 13 lines
Quoted 7 lines

With this change, the system calls do:

lazy_mmu_mode_enable_pte()
...
<store new PTE values in the per-CPU cache>
...
lazy_mmu_mode_disable() // apply cache with one multi-IPTE

I think what is not necessarily immediately obvious: this approach must assure that within such a lazy mmu section there is not a single occurrence of code which doesn't use the above mentioned modified primitives to dereference page table entry pointers.

Quoted 7 lines

Directly dereferencing such pointers would bypass the cache and lead to incorrect results. Therefore we do need some mechanism which makes sure this cannot happen. Preferebly that would happen at compile time with static code analysis. Alternatively your Kasan implementation would be helpful to find something like that after-the-fact.

However in any case we need something to address this problem.

Will try to come up with something.

...

Quoted 15 lines

arch/s390/include/asm/pgtable.h enum { · line 92

Quoted 4 lines
arch/s390/include/asm/pgtable.h +3 -0
+bool ipte_batch_ptep_test_and_clear_young(struct vm_area_struct *vma,
+					  unsigned long addr, pte_t *ptep,
+					  int *res);

Could you change this to something like:

bool __ipte_batch_ptep_test_and_clear_young(struct vm_area_struct *vma,

					    unsigned long addr, pte_t *ptep,
					    int *res);

static inline
bool ipte_batch_ptep_test_and_clear_young(struct vm_area_struct *vma,
unsigned long addr, pte_t *ptep, int *res)

Quoted 7 lines
{
	if (__is_defined(__DECOMPRESSOR))
		return false;
	return __ipte_batch_ptep_test_and_clear_young(vma, addr, ptep, res);
}

? This avoids the ifdef and makes the code easier to read and change.

Yes, I also like it much better and had something similar in early versions. But backed off to keep the whole header style intact. Anyway, will do.

Quoted 25 lines

arch/s390/include/asm/pgtable.h static inline void set_pmd(pmd_t *pmdp, pmd_t pmd) · line 1055

Quoted 5 lines
arch/s390/include/asm/pgtable.h +5 -0
+static inline void set_pte(pte_t *ptep, pte_t pte)
+{
+	if (!ipte_batch_set_pte(ptep, pte))
+		__set_pte(ptep, pte);
+}

Not sure if you analyzed it, but it looks like this might be the reason why you see the fork() slowdown: every page table operation now comes with a function call, even if is not needed.

I guess it would be helpful to add an early exit to the ipte_batch() inlines. E.g. going back to the example above:

static inline
bool ipte_batch_ptep_test_and_clear_young(struct vm_area_struct *vma,
unsigned long addr, pte_t *ptep, int *res)

{
	if (__is_defined(__DECOMPRESSOR))
		return false;
--->	if (unlikely(!ipte_batch_active()))
--->		return false;
	return __ipte_batch_ptep_test_and_clear_young(vma, addr, ptep, res);
}
Quoted 2 lines

Where ipte_batch_active() would be an inline function which simply tests a bit in lowcore.

The bit check alone would not be enough - it should be done with the preemption disabled. Then it would be something like lazy_mmu_mode(), but with one or more values in the lowcore instead of percpu variable (at least ::base_pte to check whether ptep falls into the active range).

...

Quoted 6 lines
Quoted 1 line

+#define PTE_POISON 0

Is there a reason why the PTE_POISON value is zero? If it can be a different value, I would like to propose a PTE which has bit 11 set (the one which must be zero). If that would ever be transferred to page table, and it would be used, we would see that immediately :)

The main reason was to avoid explicit initialization of per-CPU arrays. But yes, bit 11 sounds like a much better approach ;)

Quoted 7 lines
Quoted 7 lines
arch/s390/mm/ipte_batch.c new file +7 -0
+struct ipte_batch {
+	struct mm_struct *mm;
+	unsigned long base_addr;
+	unsigned long base_end;
+	pte_t *base_pte;
+	pte_t *start_pte;
+	pte_t *end_pte;
Quoted 8 lines

arch/s390/mm/ipte_batch.c line 18

Quoted 4 lines
arch/s390/mm/ipte_batch.c new file +4 -0
+	pte_t cache[PTRS_PER_PTE];
+};
+
+static DEFINE_PER_CPU(struct ipte_batch, ipte_range);

This is ~1MB with 512 possible CPUs. I think it would make sense to allocate and free this data structure with CPU hotplug to avoid wasting too much memory.

Will do.

Quoted 14 lines
Quoted 14 lines
arch/s390/mm/ipte_batch.c new file +14 -0
+static int count_contiguous(pte_t *start, pte_t *end, bool *valid)
+{
+	pte_t pte = __ptep_get(start);
+	pte_t *ptep;
+
+	*valid = !(pte_val(pte) & _PAGE_INVALID);
+
+	for (ptep = start + 1; ptep < end; ptep++) {
+		pte = __ptep_get(ptep);
+		if (*valid) {
+			if (pte_val(pte) & _PAGE_INVALID)
+				break;
+		} else {
+			if (!(pte_val(pte) & _PAGE_INVALID))
Quoted 8 lines

arch/s390/mm/ipte_batch.c line 39

Quoted 6 lines
arch/s390/mm/ipte_batch.c new file +6 -0
+				break;
+		}
+	}
+
+	return ptep - start;
+}

Wouldn't it be possible to shorten this a bit? E.g.:

Will try.

Quoted 14 lines

static int count_contiguous(pte_t *start, pte_t *end, bool *valid)

{
	unsigned long inv;
	pte_t *ptep;

	inv = pte_val(__ptep_get(start)) & _PAGE_INVALID;
	*valid = !inv;
	for (ptep = start + 1; ptep < end; ptep++) {
		if (pte_val(__ptep_get(ptep)) & _PAGE_INVALID != inv)
			break;
	}
	return ptep - start;
}
Quoted 8 lines

arch/s390/mm/ipte_batch.c line 46

Quoted 6 lines
arch/s390/mm/ipte_batch.c new file +6 -0
+static void __invalidate_pte_range(struct mm_struct *mm, unsigned long addr,
+				   int nr_ptes, pte_t *ptep)
+{
+	atomic_inc(&mm->context.flush_count);
+	if (cpu_has_tlb_lc() &&
+	    cpumask_equal(mm_cpumask(mm), cpumask_of(smp_processor_id())))

One line please. Yes, I know, this is more or less copy-pasted, but... :)

Will do.

Quoted 7 lines

Just a general comment about the naming conventions: imho ipte_batch is not a nice choice, since the name of the facility is "ipte range". However I would abstract even more, since nobody knows if there will be a different instruction or facility to achieve all of this in a better way.

Anyway... maybe rename the file simply to mmu.c or tlb.c and change the function prefixes accordingly so we end up with shorter function names?

I would in turn suggest lazy_mmu_ prefix and lazy_mmu.c source name to emphasize it implements the generic lazy mmu mode. At the same time keep ipte_batch (or ipte_range rather) in the implementation itself.

Thanks a lot for the review!

maintainer
#24 lore raw
Quoted 26 lines · Alexander Gordeev
Quoted 25 lines · Heiko Carstens

arch/s390/include/asm/pgtable.h static inline void set_pmd(pmd_t *pmdp, pmd_t pmd) · line 1055

Quoted 5 lines
arch/s390/include/asm/pgtable.h +5 -0
+static inline void set_pte(pte_t *ptep, pte_t pte)
+{
+	if (!ipte_batch_set_pte(ptep, pte))
+		__set_pte(ptep, pte);
+}

Not sure if you analyzed it, but it looks like this might be the reason why you see the fork() slowdown: every page table operation now comes with a function call, even if is not needed.

I guess it would be helpful to add an early exit to the ipte_batch() inlines. E.g. going back to the example above:

static inline
bool ipte_batch_ptep_test_and_clear_young(struct vm_area_struct *vma,
unsigned long addr, pte_t *ptep, int *res)

{
	if (__is_defined(__DECOMPRESSOR))
		return false;
--->	if (unlikely(!ipte_batch_active()))
--->		return false;
	return __ipte_batch_ptep_test_and_clear_young(vma, addr, ptep, res);
}
Quoted 7 lines
Quoted 2 lines

Where ipte_batch_active() would be an inline function which simply tests a bit in lowcore.

The bit check alone would not be enough - it should be done with the preemption disabled. Then it would be something like lazy_mmu_mode(), but with one or more values in the lowcore instead of percpu variable (at least ::base_pte to check whether ptep falls into the active range).

What I meant: you need a one bit check in the inlined function, so you can decide if it is even necessary to do the function call. More checking could be done. However all the overhead of function calls will go away with single bit check.

Or in other words: enter_ipte_batch() should set a bit in lowcore which can be easily tested. This bit _also_ indicates that preemption is disabled. So all the extra preempt disable / enable pairs within the page table primitives are not needed, after this bit is present und used.

Quoted 10 lines
Quoted 7 lines

Just a general comment about the naming conventions: imho ipte_batch is not a nice choice, since the name of the facility is "ipte range". However I would abstract even more, since nobody knows if there will be a different instruction or facility to achieve all of this in a better way.

Anyway... maybe rename the file simply to mmu.c or tlb.c and change the function prefixes accordingly so we end up with shorter function names?

I would in turn suggest lazy_mmu_ prefix and lazy_mmu.c source name to emphasize it implements the generic lazy mmu mode.

Ok.

Quoted 2 lines

At the same time
keep ipte_batch (or ipte_range rather) in the implementation itself.

That still doesn't look to good to me, but can be solved at a later time.

author maintainer
#25 lore raw
In reply to Heiko Carstens
Quoted 8 lines · Heiko Carstens
Quoted 7 lines

With this change, the system calls do:

lazy_mmu_mode_enable_pte()
...
<store new PTE values in the per-CPU cache>
...
lazy_mmu_mode_disable() // apply cache with one multi-IPTE

Quoted 4 lines

I think what is not necessarily immediately obvious: this approach must assure that within such a lazy mmu section there is not a single occurrence of code which doesn't use the above mentioned modified primitives to dereference page table entry pointers.

So ideally I would do something like __private for pte_t, but I did not manage to circumvent sparse false positives neither with a scalar pte_t, nor with a struct. It is as well-known sparse limitation, if I am not mistaken.

Quoted 5 lines

Directly dereferencing such pointers would bypass the cache and lead to incorrect results. Therefore we do need some mechanism which makes sure this cannot happen. Preferebly that would happen at compile time with static code analysis. Alternatively your Kasan implementation would be helpful to find something like that after-the-fact.

So it appears the only solution ;(

Quoted 1 line

However in any case we need something to address this problem.

Thanks!

author maintainer
#26 lore raw

PATCH v2 6/6 s390/mm: Allow lazy MMU mode disabling

In reply to Alexander Gordeev

Inroduce "lazy_mmu" kernel command line parameter
to allow disabling of the lazy MMU mode on boot.

---
 arch/s390/mm/ipte_batch.c | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)
arch/s390/mm/ipte_batch.c +22 -0
index cc4c50347d0f..cd86daeba7ec 100644
@@ -16,6 +16,22 @@ struct ipte_batch {
 };
 
 static DEFINE_PER_CPU(struct ipte_batch, ipte_range);
+static DEFINE_STATIC_KEY_TRUE_RO(lazy_mmu);
+
+static int __init setup_lazy_mmu(char *str)
+{
+	bool enable;
+
+	if (kstrtobool(str, &enable)) {
+		pr_warn("Failed to setup lazy MMU mode, set to enabled\n");
+	} else if (!enable) {
+		pr_warn("Disabling lazy MMU mode\n");
+		static_key_disable(&lazy_mmu.key);
+	}
+
+	return 0;
+}
+early_param("lazy_mmu", setup_lazy_mmu);
 
 static int count_contiguous(pte_t *start, pte_t *end, bool *valid)
 {
@@ -169,6 +185,8 @@ void arch_enter_lazy_mmu_mode_for_pte_range(struct mm_struct *mm,
 {
 	if (!test_facility(13))
 		return;
+	if (!static_branch_likely(&lazy_mmu))
+		return;
 	enter_ipte_batch(mm, addr, end, pte);
 }
 EXPORT_SYMBOL_IF_KUNIT(arch_enter_lazy_mmu_mode_for_pte_range);
@@ -177,6 +195,8 @@ void arch_leave_lazy_mmu_mode(void)
 {
 	if (!test_facility(13))
 		return;
+	if (!static_branch_likely(&lazy_mmu))
+		return;
 	leave_ipte_batch();
 }
 EXPORT_SYMBOL_IF_KUNIT(arch_leave_lazy_mmu_mode);
@@ -185,6 +205,8 @@ void arch_flush_lazy_mmu_mode(void)
 {
 	if (!test_facility(13))
 		return;
+	if (!static_branch_likely(&lazy_mmu))
+		return;
 	flush_lazy_mmu_mode();
 }
 EXPORT_SYMBOL_IF_KUNIT(arch_flush_lazy_mmu_mode);
Signature · 1 line
2.51.0
maintainer
#27 lore raw

Re: [PATCH v2 6/6] s390/mm: Allow lazy MMU mode disabling

Quoted 7 lines · Alexander Gordeev

Inroduce "lazy_mmu" kernel command line parameter
to allow disabling of the lazy MMU mode on boot.

---
 arch/s390/mm/ipte_batch.c | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

...

Quoted 21 lines

static DEFINE_PER_CPU(struct ipte_batch, ipte_range);

arch/s390/mm/ipte_batch.c +16 -0
+static DEFINE_STATIC_KEY_TRUE_RO(lazy_mmu);
+
+static int __init setup_lazy_mmu(char *str)
+{
+	bool enable;
+
+	if (kstrtobool(str, &enable)) {
+		pr_warn("Failed to setup lazy MMU mode, set to enabled\n");
+	} else if (!enable) {
+		pr_warn("Disabling lazy MMU mode\n");
+		static_key_disable(&lazy_mmu.key);
+	}
+
+	return 0;
+}
+early_param("lazy_mmu", setup_lazy_mmu);
 
 static int count_contiguous(pte_t *start, pte_t *end, bool *valid)
 {

arch/s390/mm/ipte_batch.c void arch_enter_lazy_mmu_mode_for_pte_range(struct mm_struct *mm, · line 189

Quoted 5 lines
arch/s390/mm/ipte_batch.c +2 -0
@@ -169,6 +185,8 @@ void arch_enter_lazy_mmu_mode_for_pte_range(struct mm_struct *mm,
 {
 	if (!test_facility(13))
 		return;
+	if (!static_branch_likely(&lazy_mmu))
+		return;

Looks like there is an alternative chicken switch available: remove facility bit 13 from the facility list via kernel command line option.

Unless I am mistaken: that should be sufficient and avoids adding more code.

author maintainer
#28 lore raw

Re: [PATCH v2 6/6] s390/mm: Allow lazy MMU mode disabling

Quoted 10 lines · Heiko Carstens

arch/s390/mm/ipte_batch.c void arch_enter_lazy_mmu_mode_for_pte_range(struct mm_struct *mm, · line 189

Quoted 6 lines
arch/s390/mm/ipte_batch.c +2 -0
@@ -169,6 +185,8 @@ void arch_enter_lazy_mmu_mode_for_pte_range(struct mm_struct *mm,
 {
 	if (!test_facility(13))
 		return;
+	if (!static_branch_likely(&lazy_mmu))
+		return;

Looks like there is an alternative chicken switch available: remove facility bit 13 from the facility list via kernel command line option.

Quoted 2 lines

Unless I am mistaken: that should be sufficient and avoids adding more code.

But that would also withdraw multi-IPTE from other unrelated parts of the code, e.g. from __kernel_map_pages().

#29 lore raw

Re: [PATCH v2 6/6] s390/mm: Allow lazy MMU mode disabling

Am 16.04.26 um 09:00 schrieb Alexander Gordeev:

Quoted 11 lines · Heiko Carstens
Quoted 10 lines

arch/s390/mm/ipte_batch.c void arch_enter_lazy_mmu_mode_for_pte_range(struct mm_struct *mm, · line 189

Quoted 6 lines
arch/s390/mm/ipte_batch.c +2 -0
@@ -169,6 +185,8 @@ void arch_enter_lazy_mmu_mode_for_pte_range(struct mm_struct *mm,
  {
  	if (!test_facility(13))
  		return;
+	if (!static_branch_likely(&lazy_mmu))
+		return;

Looks like there is an alternative chicken switch available: remove facility bit 13 from the facility list via kernel command line option.

Quoted 5 lines
Quoted 2 lines

Unless I am mistaken: that should be sufficient and avoids adding more code.

But that would also withdraw multi-IPTE from other unrelated parts of the code, e.g. from __kernel_map_pages().

Which is fine. Do not over-optimize for a case that should not be needed unless we really mess up. This parameter is way too complicated to ever be set/unset by a customer.