《LINUX3.0内核源代码分析》第一章:内存寻址

简介: 摘要:本章主要介绍了LINUX3.0内存寻址方面的内容,重点对follow_page函数进行注释,以帮助读者大致了解ARM A9的页表组织。  读者需要理解一些基本概念:虚拟地址、物理地址、MPU、MMU、ARM中的二级页表、cache、TLB。

摘要:本章主要介绍了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 A9linux3.0代码实现。因此,需要读者有一定的ARM体系硬件知识。推荐阅读《ARM嵌入式系统开发-软件设计与优化》。另外,读者最好对内核有所了解,推荐阅读《深入理解LINUX内核》第三版。

读者需要理解一些基本概念:虚拟地址、物理地址、MPUMMUARM中的二级页表、cacheTLB

 

1.1     基本函数

Linux3.0将分页抽象为四级:

名称

数据结构

备注

页全局目录

Pgd_t

 

页上级目录

Pud_t

A9未用

页中间目录

Pmd_t

A9未用

页表

Pte_t

 

 

/**

 * A9来说,只支持4K大小的页,因此PAGE_SHIFT定义为12.它表示一个虚拟地址的页内偏移量的位数。

 * 根据它计算出来的页大小PAGE_SIZE4KPAGE_MASK0xffff000

 */

#define PAGE_SHIFT           12

#define PAGE_SIZE              (_AC(1,UL)

#define PAGE_MASK           (~(PAGE_SIZE-1))

 

/**

 * A9来说,没有PMDPUD,因此,PMD_SHIFTPUD_SHIFT的值与PGDIR_SHIFT是一样的,都是21.

 * 21表示一个页全局目录项代表了2^201M的地址空间。

 */

#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_allocpmd_freepte_alloc_mappte_free等宏或函数与此类似。

 

1.2     刷新 cache TLB

CacheCPU与内存之间的缓存,而TLBCPUMMU之间缓存。

当外部硬件通过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直接返回pudpgd

         */

        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;

}

相关文章
|
2天前
|
算法 Linux 开发者
深入探究Linux内核中的内存管理机制
本文旨在对Linux操作系统的内存管理机制进行深入分析,探讨其如何通过高效的内存分配和回收策略来优化系统性能。文章将详细介绍Linux内核中内存管理的关键技术点,包括物理内存与虚拟内存的映射、页面置换算法、以及内存碎片的处理方法等。通过对这些技术点的解析,本文旨在为读者提供一个清晰的Linux内存管理框架,帮助理解其在现代计算环境中的重要性和应用。
|
5天前
|
缓存 算法 Linux
Linux内核中的内存管理机制深度剖析####
【10月更文挑战第28天】 本文深入探讨了Linux操作系统的心脏——内核,聚焦其内存管理机制的奥秘。不同于传统摘要的概述方式,本文将以一次虚拟的内存分配请求为引子,逐步揭开Linux如何高效、安全地管理着从微小嵌入式设备到庞大数据中心数以千计程序的内存需求。通过这段旅程,读者将直观感受到Linux内存管理的精妙设计与强大能力,以及它是如何在复杂多变的环境中保持系统稳定与性能优化的。 ####
11 0
|
5月前
|
存储 Linux Android开发
Volatility3内存取证工具安装及入门在Linux下的安装教程
Volatility 是一个完全开源的工具,用于从内存 (RAM) 样本中提取数字工件。支持Windows,Linux,MaC,Android等多类型操作系统系统的内存取证。针对竞赛这块(CTF、技能大赛等)基本上都是用在Misc方向的取证题上面,很多没有听说过或者不会用这款工具的同学在打比赛的时候就很难受。以前很多赛项都是使用vol2.6都可以完成,但是由于操作系统更新,部分系统2.6已经不支持了,如:Win10 等镜像,而Volatility3是支持这些新版本操作系统的。
|
1月前
|
存储 算法 C语言
MacOS环境-手写操作系统-15-内核管理 检测可用内存
MacOS环境-手写操作系统-15-内核管理 检测可用内存
33 0
|
3月前
|
算法 安全 UED
探索操作系统的内核空间:虚拟内存管理
【7月更文挑战第50天】 在现代操作系统中,虚拟内存管理是核心功能之一,它允许操作系统高效地使用物理内存,并为应用程序提供独立的地址空间。本文将深入探讨操作系统虚拟内存管理的机制,包括分页、分段以及内存交换等关键技术,并分析它们如何共同作用以实现内存的有效管理和保护。通过理解这些原理,读者可以更好地把握操作系统的内部工作原理及其对应用程序性能的影响。
|
4月前
|
运维 Java Linux
(九)JVM成神路之性能调优、GC调试、各内存区、Linux参数大全及实用小技巧
本章节主要用于补齐之前GC篇章以及JVM运行时数据区的一些JVM参数,更多的作用也可以看作是JVM的参数列表大全。对于开发者而言,能够控制JVM的部分也就只有启动参数了,同时,对于JVM的性能调优而言,JVM的参数也是基础。
101 8
|
4月前
|
缓存 Linux 虚拟化
linux 查看服务器cpu 与内存配置
linux 查看服务器cpu 与内存配置
586 4
|
4月前
|
Arthas 存储 Java
JVM内存问题之Linux使用ptmalloc2导致的JNI内存溢出问题如何解决
JVM内存问题之Linux使用ptmalloc2导致的JNI内存溢出问题如何解决
|
5月前
|
Java Linux PHP
【应急响应】后门攻击检测指南&Rookit&内存马&权限维持&WIN&Linux
【应急响应】后门攻击检测指南&Rookit&内存马&权限维持&WIN&Linux
105 1
|
5月前
|
算法 Linux 测试技术
Linux编程:测试-高效内存复制与随机数生成的性能
该文探讨了软件工程中的性能优化,重点关注内存复制和随机数生成。文章通过测试指出,`g_memmove`在内存复制中表现出显著优势,比简单for循环快约32倍。在随机数生成方面,`GRand`库在1000万次循环中的效率超过传统`rand()`。文中提供了测试代码和Makefile,建议在性能关键场景中使用`memcpy`、`g_memmove`以及高效的随机数生成库。