![Yang Shi](/assets/img/avatar_default.png)
A few new fields were added to mmu_gather to make TLB flush smarter for huge page by telling what level of page table is changed. __tlb_reset_range() is used to reset all these page table state to unchanged, which is called by TLB flush for parallel mapping changes for the same range under non-exclusive lock (i.e. read mmap_sem). Before commitdd2283f260
("mm: mmap: zap pages with read mmap_sem in munmap"), the syscalls (e.g. MADV_DONTNEED, MADV_FREE) which may update PTEs in parallel don't remove page tables. But, the forementioned commit may do munmap() under read mmap_sem and free page tables. This may result in program hang on aarch64 reported by Jan Stancek. The problem could be reproduced by his test program with slightly modified below. ---8<--- static int map_size = 4096; static int num_iter = 500; static long threads_total; static void *distant_area; void *map_write_unmap(void *ptr) { int *fd = ptr; unsigned char *map_address; int i, j = 0; for (i = 0; i < num_iter; i++) { map_address = mmap(distant_area, (size_t) map_size, PROT_WRITE | PROT_READ, MAP_SHARED | MAP_ANONYMOUS, -1, 0); if (map_address == MAP_FAILED) { perror("mmap"); exit(1); } for (j = 0; j < map_size; j++) map_address[j] = 'b'; if (munmap(map_address, map_size) == -1) { perror("munmap"); exit(1); } } return NULL; } void *dummy(void *ptr) { return NULL; } int main(void) { pthread_t thid[2]; /* hint for mmap in map_write_unmap() */ distant_area = mmap(0, DISTANT_MMAP_SIZE, PROT_WRITE | PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); munmap(distant_area, (size_t)DISTANT_MMAP_SIZE); distant_area += DISTANT_MMAP_SIZE / 2; while (1) { pthread_create(&thid[0], NULL, map_write_unmap, NULL); pthread_create(&thid[1], NULL, dummy, NULL); pthread_join(thid[0], NULL); pthread_join(thid[1], NULL); } } ---8<--- The program may bring in parallel execution like below: t1 t2 munmap(map_address) downgrade_write(&mm->mmap_sem); unmap_region() tlb_gather_mmu() inc_tlb_flush_pending(tlb->mm); free_pgtables() tlb->freed_tables = 1 tlb->cleared_pmds = 1 pthread_exit() madvise(thread_stack, 8M, MADV_DONTNEED) zap_page_range() tlb_gather_mmu() inc_tlb_flush_pending(tlb->mm); tlb_finish_mmu() if (mm_tlb_flush_nested(tlb->mm)) __tlb_reset_range() __tlb_reset_range() would reset freed_tables and cleared_* bits, but this may cause inconsistency for munmap() which do free page tables. Then it may result in some architectures, e.g. aarch64, may not flush TLB completely as expected to have stale TLB entries remained. Use fullmm flush since it yields much better performance on aarch64 and non-fullmm doesn't yields significant difference on x86. The original proposed fix came from Jan Stancek who mainly debugged this issue, I just wrapped up everything together. Jan's testing results: v5.2-rc2-24-gbec7550cca10 -------------------------- mean stddev real 37.382 2.780 user 1.420 0.078 sys 54.658 1.855 v5.2-rc2-24-gbec7550cca10 + "mm: mmu_gather: remove __tlb_reset_range() for force flush" ---------------------------------------------------------------------------------------_ mean stddev real 37.119 2.105 user 1.548 0.087 sys 55.698 1.357 [akpm@linux-foundation.org: coding-style fixes] Link: http://lkml.kernel.org/r/1558322252-113575-1-git-send-email-yang.shi@linux.alibaba.com Fixes:dd2283f260
("mm: mmap: zap pages with read mmap_sem in munmap") Signed-off-by: Yang Shi <yang.shi@linux.alibaba.com> Signed-off-by: Jan Stancek <jstancek@redhat.com> Reported-by: Jan Stancek <jstancek@redhat.com> Tested-by: Jan Stancek <jstancek@redhat.com> Suggested-by: Will Deacon <will.deacon@arm.com> Tested-by: Will Deacon <will.deacon@arm.com> Acked-by: Will Deacon <will.deacon@arm.com> Cc: Peter Zijlstra <peterz@infradead.org> Cc: Nick Piggin <npiggin@gmail.com> Cc: "Aneesh Kumar K.V" <aneesh.kumar@linux.ibm.com> Cc: Nadav Amit <namit@vmware.com> Cc: Minchan Kim <minchan@kernel.org> Cc: Mel Gorman <mgorman@suse.de> Cc: <stable@vger.kernel.org> [4.20+] Signed-off-by: Andrew Morton <akpm@linux-foundation.org> Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
281 lines
6.8 KiB
C
281 lines
6.8 KiB
C
#include <linux/gfp.h>
|
|
#include <linux/highmem.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/mmdebug.h>
|
|
#include <linux/mm_types.h>
|
|
#include <linux/pagemap.h>
|
|
#include <linux/rcupdate.h>
|
|
#include <linux/smp.h>
|
|
#include <linux/swap.h>
|
|
|
|
#include <asm/pgalloc.h>
|
|
#include <asm/tlb.h>
|
|
|
|
#ifndef CONFIG_HAVE_MMU_GATHER_NO_GATHER
|
|
|
|
static bool tlb_next_batch(struct mmu_gather *tlb)
|
|
{
|
|
struct mmu_gather_batch *batch;
|
|
|
|
batch = tlb->active;
|
|
if (batch->next) {
|
|
tlb->active = batch->next;
|
|
return true;
|
|
}
|
|
|
|
if (tlb->batch_count == MAX_GATHER_BATCH_COUNT)
|
|
return false;
|
|
|
|
batch = (void *)__get_free_pages(GFP_NOWAIT | __GFP_NOWARN, 0);
|
|
if (!batch)
|
|
return false;
|
|
|
|
tlb->batch_count++;
|
|
batch->next = NULL;
|
|
batch->nr = 0;
|
|
batch->max = MAX_GATHER_BATCH;
|
|
|
|
tlb->active->next = batch;
|
|
tlb->active = batch;
|
|
|
|
return true;
|
|
}
|
|
|
|
static void tlb_batch_pages_flush(struct mmu_gather *tlb)
|
|
{
|
|
struct mmu_gather_batch *batch;
|
|
|
|
for (batch = &tlb->local; batch && batch->nr; batch = batch->next) {
|
|
free_pages_and_swap_cache(batch->pages, batch->nr);
|
|
batch->nr = 0;
|
|
}
|
|
tlb->active = &tlb->local;
|
|
}
|
|
|
|
static void tlb_batch_list_free(struct mmu_gather *tlb)
|
|
{
|
|
struct mmu_gather_batch *batch, *next;
|
|
|
|
for (batch = tlb->local.next; batch; batch = next) {
|
|
next = batch->next;
|
|
free_pages((unsigned long)batch, 0);
|
|
}
|
|
tlb->local.next = NULL;
|
|
}
|
|
|
|
bool __tlb_remove_page_size(struct mmu_gather *tlb, struct page *page, int page_size)
|
|
{
|
|
struct mmu_gather_batch *batch;
|
|
|
|
VM_BUG_ON(!tlb->end);
|
|
|
|
#ifdef CONFIG_HAVE_MMU_GATHER_PAGE_SIZE
|
|
VM_WARN_ON(tlb->page_size != page_size);
|
|
#endif
|
|
|
|
batch = tlb->active;
|
|
/*
|
|
* Add the page and check if we are full. If so
|
|
* force a flush.
|
|
*/
|
|
batch->pages[batch->nr++] = page;
|
|
if (batch->nr == batch->max) {
|
|
if (!tlb_next_batch(tlb))
|
|
return true;
|
|
batch = tlb->active;
|
|
}
|
|
VM_BUG_ON_PAGE(batch->nr > batch->max, page);
|
|
|
|
return false;
|
|
}
|
|
|
|
#endif /* HAVE_MMU_GATHER_NO_GATHER */
|
|
|
|
#ifdef CONFIG_HAVE_RCU_TABLE_FREE
|
|
|
|
/*
|
|
* See the comment near struct mmu_table_batch.
|
|
*/
|
|
|
|
/*
|
|
* If we want tlb_remove_table() to imply TLB invalidates.
|
|
*/
|
|
static inline void tlb_table_invalidate(struct mmu_gather *tlb)
|
|
{
|
|
#ifndef CONFIG_HAVE_RCU_TABLE_NO_INVALIDATE
|
|
/*
|
|
* Invalidate page-table caches used by hardware walkers. Then we still
|
|
* need to RCU-sched wait while freeing the pages because software
|
|
* walkers can still be in-flight.
|
|
*/
|
|
tlb_flush_mmu_tlbonly(tlb);
|
|
#endif
|
|
}
|
|
|
|
static void tlb_remove_table_smp_sync(void *arg)
|
|
{
|
|
/* Simply deliver the interrupt */
|
|
}
|
|
|
|
static void tlb_remove_table_one(void *table)
|
|
{
|
|
/*
|
|
* This isn't an RCU grace period and hence the page-tables cannot be
|
|
* assumed to be actually RCU-freed.
|
|
*
|
|
* It is however sufficient for software page-table walkers that rely on
|
|
* IRQ disabling. See the comment near struct mmu_table_batch.
|
|
*/
|
|
smp_call_function(tlb_remove_table_smp_sync, NULL, 1);
|
|
__tlb_remove_table(table);
|
|
}
|
|
|
|
static void tlb_remove_table_rcu(struct rcu_head *head)
|
|
{
|
|
struct mmu_table_batch *batch;
|
|
int i;
|
|
|
|
batch = container_of(head, struct mmu_table_batch, rcu);
|
|
|
|
for (i = 0; i < batch->nr; i++)
|
|
__tlb_remove_table(batch->tables[i]);
|
|
|
|
free_page((unsigned long)batch);
|
|
}
|
|
|
|
static void tlb_table_flush(struct mmu_gather *tlb)
|
|
{
|
|
struct mmu_table_batch **batch = &tlb->batch;
|
|
|
|
if (*batch) {
|
|
tlb_table_invalidate(tlb);
|
|
call_rcu(&(*batch)->rcu, tlb_remove_table_rcu);
|
|
*batch = NULL;
|
|
}
|
|
}
|
|
|
|
void tlb_remove_table(struct mmu_gather *tlb, void *table)
|
|
{
|
|
struct mmu_table_batch **batch = &tlb->batch;
|
|
|
|
if (*batch == NULL) {
|
|
*batch = (struct mmu_table_batch *)__get_free_page(GFP_NOWAIT | __GFP_NOWARN);
|
|
if (*batch == NULL) {
|
|
tlb_table_invalidate(tlb);
|
|
tlb_remove_table_one(table);
|
|
return;
|
|
}
|
|
(*batch)->nr = 0;
|
|
}
|
|
|
|
(*batch)->tables[(*batch)->nr++] = table;
|
|
if ((*batch)->nr == MAX_TABLE_BATCH)
|
|
tlb_table_flush(tlb);
|
|
}
|
|
|
|
#endif /* CONFIG_HAVE_RCU_TABLE_FREE */
|
|
|
|
static void tlb_flush_mmu_free(struct mmu_gather *tlb)
|
|
{
|
|
#ifdef CONFIG_HAVE_RCU_TABLE_FREE
|
|
tlb_table_flush(tlb);
|
|
#endif
|
|
#ifndef CONFIG_HAVE_MMU_GATHER_NO_GATHER
|
|
tlb_batch_pages_flush(tlb);
|
|
#endif
|
|
}
|
|
|
|
void tlb_flush_mmu(struct mmu_gather *tlb)
|
|
{
|
|
tlb_flush_mmu_tlbonly(tlb);
|
|
tlb_flush_mmu_free(tlb);
|
|
}
|
|
|
|
/**
|
|
* tlb_gather_mmu - initialize an mmu_gather structure for page-table tear-down
|
|
* @tlb: the mmu_gather structure to initialize
|
|
* @mm: the mm_struct of the target address space
|
|
* @start: start of the region that will be removed from the page-table
|
|
* @end: end of the region that will be removed from the page-table
|
|
*
|
|
* Called to initialize an (on-stack) mmu_gather structure for page-table
|
|
* tear-down from @mm. The @start and @end are set to 0 and -1
|
|
* respectively when @mm is without users and we're going to destroy
|
|
* the full address space (exit/execve).
|
|
*/
|
|
void tlb_gather_mmu(struct mmu_gather *tlb, struct mm_struct *mm,
|
|
unsigned long start, unsigned long end)
|
|
{
|
|
tlb->mm = mm;
|
|
|
|
/* Is it from 0 to ~0? */
|
|
tlb->fullmm = !(start | (end+1));
|
|
|
|
#ifndef CONFIG_HAVE_MMU_GATHER_NO_GATHER
|
|
tlb->need_flush_all = 0;
|
|
tlb->local.next = NULL;
|
|
tlb->local.nr = 0;
|
|
tlb->local.max = ARRAY_SIZE(tlb->__pages);
|
|
tlb->active = &tlb->local;
|
|
tlb->batch_count = 0;
|
|
#endif
|
|
|
|
#ifdef CONFIG_HAVE_RCU_TABLE_FREE
|
|
tlb->batch = NULL;
|
|
#endif
|
|
#ifdef CONFIG_HAVE_MMU_GATHER_PAGE_SIZE
|
|
tlb->page_size = 0;
|
|
#endif
|
|
|
|
__tlb_reset_range(tlb);
|
|
inc_tlb_flush_pending(tlb->mm);
|
|
}
|
|
|
|
/**
|
|
* tlb_finish_mmu - finish an mmu_gather structure
|
|
* @tlb: the mmu_gather structure to finish
|
|
* @start: start of the region that will be removed from the page-table
|
|
* @end: end of the region that will be removed from the page-table
|
|
*
|
|
* Called at the end of the shootdown operation to free up any resources that
|
|
* were required.
|
|
*/
|
|
void tlb_finish_mmu(struct mmu_gather *tlb,
|
|
unsigned long start, unsigned long end)
|
|
{
|
|
/*
|
|
* If there are parallel threads are doing PTE changes on same range
|
|
* under non-exclusive lock (e.g., mmap_sem read-side) but defer TLB
|
|
* flush by batching, one thread may end up seeing inconsistent PTEs
|
|
* and result in having stale TLB entries. So flush TLB forcefully
|
|
* if we detect parallel PTE batching threads.
|
|
*
|
|
* However, some syscalls, e.g. munmap(), may free page tables, this
|
|
* needs force flush everything in the given range. Otherwise this
|
|
* may result in having stale TLB entries for some architectures,
|
|
* e.g. aarch64, that could specify flush what level TLB.
|
|
*/
|
|
if (mm_tlb_flush_nested(tlb->mm)) {
|
|
/*
|
|
* The aarch64 yields better performance with fullmm by
|
|
* avoiding multiple CPUs spamming TLBI messages at the
|
|
* same time.
|
|
*
|
|
* On x86 non-fullmm doesn't yield significant difference
|
|
* against fullmm.
|
|
*/
|
|
tlb->fullmm = 1;
|
|
__tlb_reset_range(tlb);
|
|
tlb->freed_tables = 1;
|
|
}
|
|
|
|
tlb_flush_mmu(tlb);
|
|
|
|
/* keep the page table cache within bounds */
|
|
check_pgt_cache();
|
|
#ifndef CONFIG_HAVE_MMU_GATHER_NO_GATHER
|
|
tlb_batch_list_free(tlb);
|
|
#endif
|
|
dec_tlb_flush_pending(tlb->mm);
|
|
}
|