摘要:本章主要介绍了LINUX3.0内存寻址方面的内容,重点对follow_page函数进行注释,以帮助读者大致了解ARM A9的页表组织。 读者需要理解一些基本概念:虚拟地址、物理地址、MPU、MMU、ARM中的二级页表、cache、TLB。
法律声明:《LINUX3.0内核源代码分析》系列文章由谢宝友(scxby@163.com)发表于http://xiebaoyou.blog.chinaunix.net,文章中的LINUX3.0源代码遵循GPL协议。除此以外,文档中的其他内容由作者保留所有版权。谢绝转载。
本连载文章并不是为了形成一本适合出版的书籍,而是为了向有一定内核基本的读者提供一些linux3.0源码分析。因此,请读者结合《深入理解LINUX内核》第三版阅读本连载。
本系列文章分析ARM A9的linux3.0代码实现。因此,需要读者有一定的ARM体系硬件知识。推荐阅读《ARM嵌入式系统开发-软件设计与优化》。另外,读者最好对内核有所了解,推荐阅读《深入理解LINUX内核》第三版。
读者需要理解一些基本概念:虚拟地址、物理地址、MPU、MMU、ARM中的二级页表、cache、TLB。
1.1 基本函数
Linux3.0将分页抽象为四级:
名称 |
数据结构 |
备注 |
页全局目录 |
Pgd_t |
|
页上级目录 |
Pud_t |
A9未用 |
页中间目录 |
Pmd_t |
A9未用 |
页表 |
Pte_t |
|
/**
* 对A9来说,只支持4K大小的页,因此PAGE_SHIFT定义为12.它表示一个虚拟地址的页内偏移量的位数。
* 根据它计算出来的页大小PAGE_SIZE为4K,PAGE_MASK为0xffff000。
*/
#define PAGE_SHIFT 12
#define PAGE_SIZE (_AC(1,UL)
#define PAGE_MASK (~(PAGE_SIZE-1))
/**
* 对A9来说,没有PMD和PUD,因此,PMD_SHIFT和PUD_SHIFT的值与PGDIR_SHIFT是一样的,都是21.
* 21表示一个页全局目录项代表了2^20即1M的地址空间。
*/
#define PMD_SHIFT 21
#define PGDIR_SHIFT 21
/**
* 分别代表一个页表、页中间目录、页全局目录表中表项的个数。
*/
#define PTRS_PER_PTE 512
#define PTRS_PER_PMD 1
#define PTRS_PER_PGD 2048
/**
* 将pte\pmd\pud\pgd\pgprot转换为整型值
*/
#define pte_val(x) (x)
#define pmd_val(x) (x)
#define pgd_val(x) ((x)[0])
#define pgprot_val(x) (x)
/**
* 将整型值转换为pte\pmd\pud\pgd\pgprot
*/
#define __pte(x) (x)
#define __pmd(x) (x)
#define __pgprot(x) (x)
1.1.1 判断页表项标志的函数
/**
* 页表项是否为0
*/
#define pte_none(pte) (!pte_val(pte))
/**
* 页表项是否可用。当页在内存中但是不可读写时置此标志。典型的用途是写时复制。
*/
#define pte_present(pte) (pte_val(pte) & L_PTE_PRESENT)
/**
* 页表项是否有可写标志
*/
#define pte_write(pte) (!(pte_val(pte) & L_PTE_RDONLY))
/**
* 页表项是否为脏
*/
#define pte_dirty(pte) (pte_val(pte) & L_PTE_DIRTY)
/**
* 页表项是否表示最近没有被访问过
*/
#define pte_young(pte) (pte_val(pte) & L_PTE_YOUNG)
/**
* 页表项是否有可执行标志
*/
#define pte_exec(pte) (!(pte_val(pte) & L_PTE_XN))
#define pte_special(pte) (0)
/**
* 清除页表项的值。
*/
#define pte_clear(mm,addr,ptep) set_pte_ext(ptep, __pte(0), 0)
/**
* 向一个页表项中写入指定的值。
*/
#define set_pte_ext(ptep,pte,ext) cpu_set_pte_ext(ptep,pte,ext)
/**
* 判断两个页表项是否指向相同的页并且有相同的访问权限
*/
static inline int pte_same(pte_t pte_a, pte_t pte_b)
{
return pte_val(pte_a) == pte_val(pte_b);
}
/**
* 检查页中间目录项是否指向不可用的页表。
*/
#define pmd_bad(pmd) (pmd_val(pmd) & 2)
/**
* 页表项是否可用。当页在内存中但是不可读写时置此标志。典型的用途是写时复制。
*/
#define pte_present(pte) (pte_val(pte) & L_PTE_PRESENT)
1.1.2 页表项操作函数
/**
* 虚拟地址在页全局目录中索引
*/
#define pgd_index(addr) ((addr) >> PGDIR_SHIFT)
/**
* 计算一个进程用户态地址对应的页全局目录项地址。
* 计算内核态地址的页全局目录项地址应当使用pgd_offset_k
*/
#define pgd_offset(mm, addr) ((mm)->pgd + pgd_index(addr))
/* to find an entry in a kernel page-table-directory */
/**
* 计算一个内核态地址的页全局目录项地址。
*/
#define pgd_offset_k(addr) pgd_offset(&init_mm, addr)
/**
* 获得页全局目录项所指向的页面。对A9来说,就是pmd_page
*/
#define pgd_page(pgd) (pud_page((pud_t){ pgd }))
/**
* 获得页全局目录项的虚拟地址。
*/
#define pgd_page_vaddr(pgd) (pud_page_vaddr((pud_t){ pgd }))
/**
* 在页全局目录表中,查找一个虚拟地址对应的页上级目录位置。
* 对二级页表来说,页上级目录就是页全局目录,因此直接返回页全局目录。
*/
#define pud_offset(pgd, start) (pgd)
/**
* 获得页上级目录页面。
*/
#define pud_page(pud) pgd_page(pud)
/**
* 获得页上级目录页面的虚拟地址。
*/
#define pud_page_vaddr(pud) pgd_page_vaddr(pud)
/**
* 获得一个虚拟地址的页中间目录中的地址。对二级页表来说,没有pmd,直接返回页全局目录地址即可。
*/
#define pmd_offset(dir, addr) ((pmd_t *)(dir))
/**
* 获得页中间目录指向的页表页面。
*/
#define pmd_page(pmd) pfn_to_page(__phys_to_pfn(pmd_val(pmd)))
/**
* 获得一个线性地址对应的页表项在页表中的索引
*/
#define pte_index(addr) (((addr) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
/**
* 在主内核页表中定位内核地址对应的页表项的虚拟地址。
*/
#define pte_offset_kernel(pmd,addr) (pmd_page_vaddr(*(pmd)) + pte_index(addr))
/**
* 在进程页表中定位线性地址对应的页表项的地址。如果页表保存在高端内存中,那么还为页表建立一个临时内核映射。
*/
#define pte_offset_map(pmd,addr) (__pte_map(pmd) + pte_index(addr))
/**
* 如果页表在高端内存中,不解除由pte_offset_map建立的临时内核映射。
*/
#define pte_unmap(pte) __pte_unmap(pte)
/**
* 获取页表项中的页帧号。
*/
#define pte_pfn(pte) (pte_val(pte) >> PAGE_SHIFT)
/**
* 根据页帧号和页面属性,合成页表项。
*/
#define pfn_pte(pfn,prot) __pte(__pfn_to_phys(pfn) | pgprot_val(prot))
/**
* 从页表项中提取页帧号,并定位该页帧号对应的页框。
*/
#define pte_page(pte) pfn_to_page(pte_pfn(pte))
/**
* 根据页框和页面属性,合成页表项。
*/
#define mk_pte(page,prot) pfn_pte(page_to_pfn(page), prot)
/**
* 当页表项映射到文件,并且没有装载进内存时,从页表项中提取文件页号。
*/
#define pte_to_pgoff(x) (pte_val(x) >> 3)
/**
* 将页面映射的页号存放到页表项中
*/
#define pgoff_to_pte(x) __pte(((x)
1.1.3 页表分配相关的函数
/**
* 为页全局目录分配内存
*/
pgd_t *pgd_alloc(struct mm_struct *mm)
/**
* 释放页全局目录项
*/
void pgd_free(struct mm_struct *mm, pgd_t *pgd_base)
/**
* 分配页上级目录,在二级页表中,此函数什么也不做。
*/
#define pud_alloc(mm, pgd, address) (pgd)
/**
* 释放页上级目录,在二级页表中,这个函数什么也不做
*/
#define pud_free(mm, x) do { } while (0)
Pmd_alloc、pmd_free、pte_alloc_map、pte_free等宏或函数与此类似。
1.2 刷新 cache 和 TLB
Cache是CPU与内存之间的缓存,而TLB是CPU与MMU之间缓存。
当外部硬件通过DMA修改了内存中的数据时,需要使cache中的数据失效,强制CPU从内存中装载数据。当CPU向缓存中写入数据后,为了通过DMA将数据传送到外部硬件,则需要将缓存中的数据强制写入内存。
当页表项映射的页面发生变化后,也需要将页面缓存的内容写入内存。
同理,当修改了页表项后,为了避免TLB中缓存的项进行错误的MMU转换,也需要使TLB中缓存的项失效。
1.3 follow_page 函数
follow_page函数是从进程的页表中搜索特定地址对应的页面对象。这个函数对于理解LINUX内核页表管理有帮助。
struct page *follow_page(struct vm_area_struct *vma, unsigned long address,
unsigned int flags)
{
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
pte_t *ptep, pte;
spinlock_t *ptl;
struct page *page;
struct mm_struct *mm = vma->vm_mm;
/**
* 对ARM A9来说,没有配置巨页功能,follow_huge_addr实际上是空处理。
*/
page = follow_huge_addr(mm, address, flags & FOLL_WRITE);
if (!IS_ERR(page)) {
BUG_ON(flags & FOLL_GET);
goto out;
}
page = NULL;
/**
* 在一级目录项中,查找地址对应的一级目录索引项。
*/
pgd = pgd_offset(mm, address);
/**
* 该地址对应的一级目录项无效。对ARM来说,pgd_none总返回0,真正的判断是在pmd_none。
*/
if (pgd_none(*pgd) || unlikely(pgd_bad(*pgd)))
goto no_page_table;
/**
* 查找地址对应的页上级目录项。这对4级目录的分组体系来说才有效。ARM不存在页上级目录和页中间目录。
* pud总是返回pgd。
*/
pud = pud_offset(pgd, address);
/**
* pud_none总是返回0,因此下面的判断是无用。真正有用的判断在后面的pmd_none
*/
if (pud_none(*pud))
goto no_page_table;
if (pud_huge(*pud) && vma->vm_flags & VM_HUGETLB) {
BUG_ON(flags & FOLL_GET);
page = follow_huge_pud(mm, address, pud, flags & FOLL_WRITE);
goto out;
}
if (unlikely(pud_bad(*pud)))
goto no_page_table;
/**
* 取页中间目录,对ARM来说,pmd直接返回pud,即pgd。
*/
pmd = pmd_offset(pud, address);
/**
* 判断pmd是否为0,即ARM一级目录是否有效。对pgd,pud的判断都是无用的,真正的判断在这里。
*/
if (pmd_none(*pmd))
goto no_page_table;
/**
* 判断pmd是否是一个巨页,以及用户虚拟地址空间段是否是一个巨页段,略过。
*/
if (pmd_huge(*pmd) && vma->vm_flags & VM_HUGETLB) {
BUG_ON(flags & FOLL_GET);
/**
* 查找巨页地址映射的物理页面。
*/
page = follow_huge_pmd(mm, address, pmd, flags & FOLL_WRITE);
goto out;
}
/**
* 透明巨页处理,对某些体系结构,如mips来说,这个功能是有效的。但是虽然ARM硬件支持巨页(1M页)
* 目前的内核还不支持ARM巨页,略过。
*/
if (pmd_trans_huge(*pmd)) {
if (flags & FOLL_SPLIT) {
split_huge_page_pmd(mm, pmd);
goto split_fallthrough;
}
spin_lock(&mm->page_table_lock);
if (likely(pmd_trans_huge(*pmd))) {
if (unlikely(pmd_trans_splitting(*pmd))) {
spin_unlock(&mm->page_table_lock);
wait_split_huge_page(vma->anon_vma, pmd);
} else {
page = follow_trans_huge_pmd(mm, address,
pmd, flags);
spin_unlock(&mm->page_table_lock);
goto out;
}
} else
spin_unlock(&mm->page_table_lock);
/* fall through */
}
split_fallthrough:
/**
* 判断pmd是否有效。
*/
if (unlikely(pmd_bad(*pmd)))
goto no_page_table;
/**
* 在二级页表中找到地址对应的pte。并将pte指针返回。
* 注意,这里获取了进程的内存页表锁。以防止内核其他路径修改进程页表,使得ptep指向的pte产生变化。
* ptl是内存页表锁。
* 如果内核支持将pte表放到高端内存,那么还需要调用kmap_atomic将页表到内核地址空间中。
*/
ptep = pte_offset_map_lock(mm, pmd, address, &ptl);
pte = *ptep;
/**
* 这里判断页表项是否有效。
* 有时,页面在内存中,但是不允许访问。比如写时复制。
* 当页完全不在内存中时,页表项也没有效。
*/
if (!pte_present(pte))
goto no_page;
/**
* 希望搜索一个可写的页面,但是页表项没有写权限。
*/
if ((flags & FOLL_WRITE) && !pte_write(pte))
goto unlock;
/**
* 根据pte中保存的页帧号,找到该页帧号对应的page结构。
*/
page = vm_normal_page(vma, address, pte);
if (unlikely(!page)) {/* 根据页帧号无法找到page结构,可能是一些特殊情况。如驱动自行管理的pte出了问题。 */
if ((flags & FOLL_DUMP) || /* 不允许返回0页 */
!is_zero_pfn(pte_pfn(pte))) /* 不是0页 */
goto bad_page;
page = pte_page(pte);/* 向上层返回0页 */
}
/**
* 调用者要求获取页面引用,则增加页面引用计数。
*/
if (flags & FOLL_GET)
get_page(page);
if (flags & FOLL_TOUCH) {/* 调用者希望设置访问标志,可能是随后会写页面 */
if ((flags & FOLL_WRITE) &&/* 获取写引用 */
!pte_dirty(pte) && !PageDirty(page))/* 页面和pte的脏标志都还没有设置,则强制设置脏标志 */
set_page_dirty(page);
/*
* pte_mkyoung() would be more correct here, but atomic care
* is needed to avoid losing the dirty bit: it is easier to use
* mark_page_accessed().
*/
/**
* 标记页面访问标志。
*/
mark_page_accessed(page);
}
/**
* 调用者想将页面锁在内存中。
*/
if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) {
/*
* The preliminary mapping check is mainly to avoid the
* pointless overhead of lock_page on the ZERO_PAGE
* which might bounce very badly if there is contention.
*
* If the page is already locked, we don't need to
* handle it now - vmscan will handle it later if and
* when it attempts to reclaim the page.
*/
if (page->mapping && trylock_page(page)) {/* 锁住页面,不交换到外部存储器中 */
lru_add_drain(); /* push cached pages to LRU */
/*
* Because we lock page here and migration is
* blocked by the pte's page reference, we need
* only check for file-cache page truncation.
*/
if (page->mapping)
mlock_vma_page(page);
unlock_page(page);
}
}
unlock:
/**
* 释放进程页面锁,同时,如果支持将页表放到高端内存,就解除对页表的映射。
*/
pte_unmap_unlock(ptep, ptl);
out:
return page;
bad_page:
pte_unmap_unlock(ptep, ptl);
return ERR_PTR(-EFAULT);
no_page:
pte_unmap_unlock(ptep, ptl);
if (!pte_none(pte))
return page;
no_page_table:
/*
* When core dumping an enormous anonymous area that nobody
* has touched so far, we don't want to allocate unnecessary pages or
* page tables. Return error instead of NULL to skip handle_mm_fault,
* then get_dump_page() will return NULL to leave a hole in the dump.
* But we can only make this optimization where a hole would surely
* be zero-filled if handle_mm_fault() actually did handle it.
*/
if ((flags & FOLL_DUMP) &&
(!vma->vm_ops || !vma->vm_ops->fault))
return ERR_PTR(-EFAULT);
return page;
}