| /* |
| * arch/arm64/mm/hugetlbpage.c |
| * |
| * Copyright (C) 2013 Linaro Ltd. |
| * |
| * Based on arch/x86/mm/hugetlbpage.c. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 as |
| * published by the Free Software Foundation. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| */ |
| |
| #include <linux/init.h> |
| #include <linux/fs.h> |
| #include <linux/mm.h> |
| #include <linux/hugetlb.h> |
| #include <linux/pagemap.h> |
| #include <linux/err.h> |
| #include <linux/sysctl.h> |
| #include <asm/mman.h> |
| #include <asm/tlb.h> |
| #include <asm/tlbflush.h> |
| #include <asm/pgalloc.h> |
| |
| int pmd_huge(pmd_t pmd) |
| { |
| return pmd_val(pmd) && !(pmd_val(pmd) & PMD_TABLE_BIT); |
| } |
| |
| int pud_huge(pud_t pud) |
| { |
| #ifndef __PAGETABLE_PMD_FOLDED |
| return pud_val(pud) && !(pud_val(pud) & PUD_TABLE_BIT); |
| #else |
| return 0; |
| #endif |
| } |
| |
| static int find_num_contig(struct mm_struct *mm, unsigned long addr, |
| pte_t *ptep, pte_t pte, size_t *pgsize) |
| { |
| pgd_t *pgd = pgd_offset(mm, addr); |
| pud_t *pud; |
| pmd_t *pmd; |
| |
| *pgsize = PAGE_SIZE; |
| if (!pte_cont(pte)) |
| return 1; |
| if (!pgd_present(*pgd)) { |
| VM_BUG_ON(!pgd_present(*pgd)); |
| return 1; |
| } |
| pud = pud_offset(pgd, addr); |
| if (!pud_present(*pud)) { |
| VM_BUG_ON(!pud_present(*pud)); |
| return 1; |
| } |
| pmd = pmd_offset(pud, addr); |
| if (!pmd_present(*pmd)) { |
| VM_BUG_ON(!pmd_present(*pmd)); |
| return 1; |
| } |
| if ((pte_t *)pmd == ptep) { |
| *pgsize = PMD_SIZE; |
| return CONT_PMDS; |
| } |
| return CONT_PTES; |
| } |
| |
| void set_huge_pte_at(struct mm_struct *mm, unsigned long addr, |
| pte_t *ptep, pte_t pte) |
| { |
| size_t pgsize; |
| int i; |
| int ncontig = find_num_contig(mm, addr, ptep, pte, &pgsize); |
| unsigned long pfn; |
| pgprot_t hugeprot; |
| |
| if (ncontig == 1) { |
| set_pte_at(mm, addr, ptep, pte); |
| return; |
| } |
| |
| pfn = pte_pfn(pte); |
| hugeprot = __pgprot(pte_val(pfn_pte(pfn, __pgprot(0))) ^ pte_val(pte)); |
| for (i = 0; i < ncontig; i++) { |
| pr_debug("%s: set pte %p to 0x%llx\n", __func__, ptep, |
| pte_val(pfn_pte(pfn, hugeprot))); |
| set_pte_at(mm, addr, ptep, pfn_pte(pfn, hugeprot)); |
| ptep++; |
| pfn += pgsize >> PAGE_SHIFT; |
| addr += pgsize; |
| } |
| } |
| |
| pte_t *huge_pte_alloc(struct mm_struct *mm, |
| unsigned long addr, unsigned long sz) |
| { |
| pgd_t *pgd; |
| pud_t *pud; |
| pte_t *pte = NULL; |
| |
| pr_debug("%s: addr:0x%lx sz:0x%lx\n", __func__, addr, sz); |
| pgd = pgd_offset(mm, addr); |
| pud = pud_alloc(mm, pgd, addr); |
| if (!pud) |
| return NULL; |
| |
| if (sz == PUD_SIZE) { |
| pte = (pte_t *)pud; |
| } else if (sz == (PAGE_SIZE * CONT_PTES)) { |
| pmd_t *pmd = pmd_alloc(mm, pud, addr); |
| |
| WARN_ON(addr & (sz - 1)); |
| /* |
| * Note that if this code were ever ported to the |
| * 32-bit arm platform then it will cause trouble in |
| * the case where CONFIG_HIGHPTE is set, since there |
| * will be no pte_unmap() to correspond with this |
| * pte_alloc_map(). |
| */ |
| pte = pte_alloc_map(mm, NULL, pmd, addr); |
| } else if (sz == PMD_SIZE) { |
| if (IS_ENABLED(CONFIG_ARCH_WANT_HUGE_PMD_SHARE) && |
| pud_none(*pud)) |
| pte = huge_pmd_share(mm, addr, pud); |
| else |
| pte = (pte_t *)pmd_alloc(mm, pud, addr); |
| } else if (sz == (PMD_SIZE * CONT_PMDS)) { |
| pmd_t *pmd; |
| |
| pmd = pmd_alloc(mm, pud, addr); |
| WARN_ON(addr & (sz - 1)); |
| return (pte_t *)pmd; |
| } |
| |
| pr_debug("%s: addr:0x%lx sz:0x%lx ret pte=%p/0x%llx\n", __func__, addr, |
| sz, pte, pte_val(*pte)); |
| return pte; |
| } |
| |
| pte_t *huge_pte_offset(struct mm_struct *mm, unsigned long addr) |
| { |
| pgd_t *pgd; |
| pud_t *pud; |
| pmd_t *pmd = NULL; |
| pte_t *pte = NULL; |
| |
| pgd = pgd_offset(mm, addr); |
| pr_debug("%s: addr:0x%lx pgd:%p\n", __func__, addr, pgd); |
| if (!pgd_present(*pgd)) |
| return NULL; |
| pud = pud_offset(pgd, addr); |
| if (!pud_present(*pud)) |
| return NULL; |
| |
| if (pud_huge(*pud)) |
| return (pte_t *)pud; |
| pmd = pmd_offset(pud, addr); |
| if (!pmd_present(*pmd)) |
| return NULL; |
| |
| if (pte_cont(pmd_pte(*pmd))) { |
| pmd = pmd_offset( |
| pud, (addr & CONT_PMD_MASK)); |
| return (pte_t *)pmd; |
| } |
| if (pmd_huge(*pmd)) |
| return (pte_t *)pmd; |
| pte = pte_offset_kernel(pmd, addr); |
| if (pte_present(*pte) && pte_cont(*pte)) { |
| pte = pte_offset_kernel( |
| pmd, (addr & CONT_PTE_MASK)); |
| return pte; |
| } |
| return NULL; |
| } |
| |
| pte_t arch_make_huge_pte(pte_t entry, struct vm_area_struct *vma, |
| struct page *page, int writable) |
| { |
| size_t pagesize = huge_page_size(hstate_vma(vma)); |
| |
| if (pagesize == CONT_PTE_SIZE) { |
| entry = pte_mkcont(entry); |
| } else if (pagesize == CONT_PMD_SIZE) { |
| entry = pmd_pte(pmd_mkcont(pte_pmd(entry))); |
| } else if (pagesize != PUD_SIZE && pagesize != PMD_SIZE) { |
| pr_warn("%s: unrecognized huge page size 0x%lx\n", |
| __func__, pagesize); |
| } |
| return entry; |
| } |
| |
| pte_t huge_ptep_get_and_clear(struct mm_struct *mm, |
| unsigned long addr, pte_t *ptep) |
| { |
| pte_t pte; |
| |
| if (pte_cont(*ptep)) { |
| int ncontig, i; |
| size_t pgsize; |
| pte_t *cpte; |
| bool is_dirty = false; |
| |
| cpte = huge_pte_offset(mm, addr); |
| ncontig = find_num_contig(mm, addr, cpte, *cpte, &pgsize); |
| /* save the 1st pte to return */ |
| pte = ptep_get_and_clear(mm, addr, cpte); |
| for (i = 1; i < ncontig; ++i) { |
| /* |
| * If HW_AFDBM is enabled, then the HW could |
| * turn on the dirty bit for any of the page |
| * in the set, so check them all. |
| */ |
| ++cpte; |
| if (pte_dirty(ptep_get_and_clear(mm, addr, cpte))) |
| is_dirty = true; |
| } |
| if (is_dirty) |
| return pte_mkdirty(pte); |
| else |
| return pte; |
| } else { |
| return ptep_get_and_clear(mm, addr, ptep); |
| } |
| } |
| |
| int huge_ptep_set_access_flags(struct vm_area_struct *vma, |
| unsigned long addr, pte_t *ptep, |
| pte_t pte, int dirty) |
| { |
| pte_t *cpte; |
| |
| if (pte_cont(pte)) { |
| int ncontig, i, changed = 0; |
| size_t pgsize = 0; |
| unsigned long pfn = pte_pfn(pte); |
| /* Select all bits except the pfn */ |
| pgprot_t hugeprot = |
| __pgprot(pte_val(pfn_pte(pfn, __pgprot(0))) ^ |
| pte_val(pte)); |
| |
| cpte = huge_pte_offset(vma->vm_mm, addr); |
| pfn = pte_pfn(*cpte); |
| ncontig = find_num_contig(vma->vm_mm, addr, cpte, |
| *cpte, &pgsize); |
| for (i = 0; i < ncontig; ++i, ++cpte) { |
| changed = ptep_set_access_flags(vma, addr, cpte, |
| pfn_pte(pfn, |
| hugeprot), |
| dirty); |
| pfn += pgsize >> PAGE_SHIFT; |
| } |
| return changed; |
| } else { |
| return ptep_set_access_flags(vma, addr, ptep, pte, dirty); |
| } |
| } |
| |
| void huge_ptep_set_wrprotect(struct mm_struct *mm, |
| unsigned long addr, pte_t *ptep) |
| { |
| if (pte_cont(*ptep)) { |
| int ncontig, i; |
| pte_t *cpte; |
| size_t pgsize = 0; |
| |
| cpte = huge_pte_offset(mm, addr); |
| ncontig = find_num_contig(mm, addr, cpte, *cpte, &pgsize); |
| for (i = 0; i < ncontig; ++i, ++cpte) |
| ptep_set_wrprotect(mm, addr, cpte); |
| } else { |
| ptep_set_wrprotect(mm, addr, ptep); |
| } |
| } |
| |
| void huge_ptep_clear_flush(struct vm_area_struct *vma, |
| unsigned long addr, pte_t *ptep) |
| { |
| if (pte_cont(*ptep)) { |
| int ncontig, i; |
| pte_t *cpte; |
| size_t pgsize = 0; |
| |
| cpte = huge_pte_offset(vma->vm_mm, addr); |
| ncontig = find_num_contig(vma->vm_mm, addr, cpte, |
| *cpte, &pgsize); |
| for (i = 0; i < ncontig; ++i, ++cpte) |
| ptep_clear_flush(vma, addr, cpte); |
| } else { |
| ptep_clear_flush(vma, addr, ptep); |
| } |
| } |
| |
| static __init int setup_hugepagesz(char *opt) |
| { |
| unsigned long ps = memparse(opt, &opt); |
| |
| if (ps == PMD_SIZE) { |
| hugetlb_add_hstate(PMD_SHIFT - PAGE_SHIFT); |
| } else if (ps == PUD_SIZE) { |
| hugetlb_add_hstate(PUD_SHIFT - PAGE_SHIFT); |
| } else if (ps == (PAGE_SIZE * CONT_PTES)) { |
| hugetlb_add_hstate(CONT_PTE_SHIFT); |
| } else if (ps == (PMD_SIZE * CONT_PMDS)) { |
| hugetlb_add_hstate((PMD_SHIFT + CONT_PMD_SHIFT) - PAGE_SHIFT); |
| } else { |
| pr_err("hugepagesz: Unsupported page size %lu K\n", ps >> 10); |
| return 0; |
| } |
| return 1; |
| } |
| __setup("hugepagesz=", setup_hugepagesz); |
| |
| #ifdef CONFIG_ARM64_64K_PAGES |
| static __init int add_default_hugepagesz(void) |
| { |
| if (size_to_hstate(CONT_PTES * PAGE_SIZE) == NULL) |
| hugetlb_add_hstate(CONT_PMD_SHIFT); |
| return 0; |
| } |
| arch_initcall(add_default_hugepagesz); |
| #endif |