Linux是为通用的操作系统而设计,为了便于移植需要抽象出一些硬件细节,在驱动代码中看到大量的抽象层的思想。内核中只有和硬件相关的代码才会单独实现,这样做便于移植和添加新硬件。
内核里所有进程和内核线程都共享1GB的地址空间,而每个应用程序对应的进程都有独立的3GB的地址空间,相互不干扰
- 用户空间:在Linux中,每个用户进程都可以访问4GB的线性地址空间,从0到3GB的虚拟地址空间是用户空间,每个用户进程通过自己的页目录,页表来直接访问
- 内核空间:从3GB到4GB的虚拟地址为内核空间,存放内核访问的代码和数据,用户进程不能访问,只有内核态进程才能访问。所有进程(包括用户进程,用户线程,内核线程)从3GB到4GB的虚拟地址空间内容都是一样的,Linux用该方式让内核进程共享代码段和数据段。
由于虚拟机制的引入,进程可以使用32位地址系统支持全部4G线性空间,进程的线性地址空间分为两部分:
- 从0x00000000到0xbfffffff的线性地址,无论用户态还是内核态的进程都可以寻址
- 从0xc0000000到0xffffffff的线性地址,只有内核态的进程能寻址
从前面章节的学习,通过内核临时页表的创建,相应的页表项已经建立号,只映射Kernel Image和DTB的物理内存,在某个还是的时候,内核需要将尽可能多的物理内存映射到页表中。尽管物理内存已经通过memblock_add
添加进系统,但是这部分的物理内存到虚拟内存的映射还没有建立,可以通过memblock_alloc
分配一段物理内存,但是还不能访问,一切还需要等待paging_init
的执行。最终页表建立好后,可以通过虚拟地址去访问最终的物理地址了。
paging_init()负责建立仅用于kernel而用户空间不可访问的页表,我们主要来看看其做了些什么?
void __init paging_init(const struct machine_desc *mdesc)
{
void *zero_page;
build_mem_type_table(); -----------------(1)
prepare_page_table(); -----------------(2)
map_lowmem(); -----------------(3)
memblock_set_current_limit(arm_lowmem_limit); -----------------(4)
dma_contiguous_remap(); -----------------(5)
early_fixmap_shutdown();
devicemaps_init(mdesc); -----------------(6)
kmap_init(); -----------------(7)
tcm_init(); -----------------(8)
top_pmd = pmd_off_k(0xffff0000);
/* allocate the zero page. */
zero_page = early_alloc(PAGE_SIZE);
bootmem_init(); -----------------(9)
empty_zero_page = virt_to_page(zero_page); -----------------(10)
__flush_dcache_page(NULL, empty_zero_page);
}
- 1.给静态全局变量mem_types赋值,这个变量就在本文件(arch/arm/mm/mmu.c)定义,它的用处就是在create_mapping函数创建映射时配置MMU硬件时需要。mem_types数组是kernel记录当前系统映射不同地址空间类型(普通内存 设备内存 IO空间等)的页表属性,其中页表属性还包括section-mapping的属性prot_sect,以及page-mapping的一级页目录属性prot_l1,二级页表属性prot_pte。这个都是与处理器相关的内容,后面章节不做介绍。
- 2.准备页表,主要是清除段映射(16K一级页表),将对应于内核映像下方以及内核空间的页目录项pmd段均清空为0
- 3.真正创建页表,重新建立从物理地址起始点到high_mem的起始点的一一映射
- 4.根据arm_lowmem_limit来作为ZONE_NORMAL的终点
- 5.建立DMA映射表
- 6.为设备IO空间和中断向量表创建页表,并刷新TLB和缓存
- 7.进行永久内存映射的初始化,存储在pkmap_page_table中,高64K是用来存放中断向量表
- 8.TCM初始化,TCM是一个固定大小的RAM,紧密地耦合至处理器内核,提供与cache相当的性能,相比于cache的优点是,程序代码可以精确地控制什么函数和代码放在哪里。
- 9.bootmem_init初始化内存管理
- 10.分配一个0页,该页用于写时复制机制。zero_page是全局变量,刷新D-CAHCE内容进RAM中。empty_zero_page是一个全局的页面数组,主要作用就是只要用户引用一个只读的匿名页面并没有进行写操作,缺页中断处理中内核就不会给用户进程分配新的页面。
1. 设置内存类型表
在build_mem_type_table函数中,根据ARM版本及内存类型,对struct mem_type结构体类型的全局数组mem_type进行初始化。mem_types结构体数组是具有L1、L2列表和缓存策略、域属性信息的内核数据结构。
为了按内存类型使用虚拟地址空间,Linux对内核使用的内存进行了分类,内核定义在arch/arm/mm/mmu.c中,其类型如下
内存类型 | 使用目的 |
---|---|
MT_DEVICE | ARMV6的共享设备 |
MT_DEVICE_NONSHARED | ARMV6的f非共享设备 |
MT_DEVICE_CACHED | 使用写缓冲及缓存设备 |
MT_DEVICE_WC | 未使用写缓冲及缓存设备 |
根据内存使用的不同目的,内存类型对是否使用缓存,是否使用写缓冲,是否共享,域等信息定义了不同的设置,这里面只是列了部分。
2. 准备页表
在内核使用内存之前,需要 初始化内核的页表 ,初始化页表主要在map_lowmem()函数中。在映射页表之前,需要把 页表的页表项清零 ,主要在prepare_page_table()函数中实现。
static inline void prepare_page_table(void)
{
unsigned long addr;
phys_addr_t end;
/*
* Clear out all the mappings below the kernel image.
*/
for (addr = 0; addr < MODULES_VADDR; addr += PMD_SIZE) ----------------(1)
pmd_clear(pmd_off_k(addr));
#ifdef CONFIG_XIP_KERNEL
/* The XIP kernel is mapped in the module area -- skip over it */
addr = ((unsigned long)_exiprom + PMD_SIZE - 1) & PMD_MASK;
#endif
for ( ; addr < PAGE_OFFSET; addr += PMD_SIZE) ----------------(2)
pmd_clear(pmd_off_k(addr));
/*
* Find the end of the first block of lowmem.
*/
end = memblock.memory.regions[0].base + memblock.memory.regions[0].size;
if (end >= arm_lowmem_limit)
end = arm_lowmem_limit;
/*
* Clear out all the kernel space mappings, except for the first
* memory bank, up to the vmalloc region.
*/
for (addr = __phys_to_virt(end); ----------------(3)
addr < VMALLOC_START; addr += PMD_SIZE)
pmd_clear(pmd_off_k(addr));
}
- 1.模块加载的范围应该是在MODULES_VADDR到MODULES_END之间,而MODULES_VADDR在文件arch/arm/include/asm/memory.h定义#define MODULES_VADDR (CONFIG_PAGE_OFFSET - SZ_8M),则该函数pmd_clear清理0~MODULES_VADDR所对应的一级页表项内容,所对应的地址为0x0 ~~~ bf000000
- 2.对PAGE_OFFSET之前的页目录项执行初始化,PAGE_OFFSET表示内核空间的起始地址,在32位系统地址空间最多为4G,在编译的时候通过Kconfig分为内核空间和用户空间,一般比为1:3,所以内核空间的起始地址为0xc000 0000。pmd_clear清理MODULES_VADDR~ PAGE_OFFSET 所对应的一级页表项内容,所对应的地址为 bf000000 ~~~ c0000000
通过下面的方式配置
config PAGE_OFFSET
hex
default PHYS_OFFSET if !MMU
default 0x40000000 if VMSPLIT_1G
default 0x80000000 if VMSPLIT_2G
default 0xB0000000 if VMSPLIT_3G_OPT
default 0xC0000000 - 3.pmd_clear清理第一个0xe0000000~0xe0800000所对应对应的8M空间的一级页表项内容
该函数是在建立完整页表前,需要对一级页目录进行清空操作,便于建立页表时,对空页表目录项进行分配。我们以imx6上模拟器为例,第一块也是唯一一块Membank是0x80000000起始地址,大小为512MB,arm_lowmem_limit也是0xa0000000。
为了初始化页目录项,需要获得要初始化的项地址,从上述的代码可以看出,pmd_clear函数将pmd_off_k函数作为输入值。正是通过pmd_off_k函数获得项地址
static inline pmd_t *pmd_off_k(unsigned long virt)
{
return pmd_offset(pud_offset(pgd_offset_k(virt), virt), virt);
}
#define pgd_offset_k(address) pgd_offset(&init_mm, (address))
#define pgd_offset(mm, addr) ((mm)->pgd + pgd_index(addr))
/* to find an entry in a page-table-directory */
#define pgd_index(addr) ((addr) >> PGDIR_SHIFT)
pdg_offset_k调用pgd_offset函数,传递的参数init_mm地址,pgd_index将输入地址addr以PGDIR_SHIFT的大小向右移动,因此会求出对应于输入地址的页目录项号,并通过pdg_offset获得管理ADDR所属内存块的页目录的相应项地址。
Init_mm根据INIT_MM进行初始化,其定义如下
struct mm_struct init_mm = {
.mm_rb = RB_ROOT,
.pgd = swapper_pg_dir,
.mm_users = ATOMIC_INIT(2),
.mm_count = ATOMIC_INIT(1),
.mmap_sem = __RWSEM_INITIALIZER(init_mm.mmap_sem),
.page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
.mmlist = LIST_HEAD_INIT(init_mm.mmlist),
INIT_MM_CONTEXT(init_mm)
};
成员 | 说明 |
---|---|
mm_rb | 虚拟内存各个区域用vm_area_struct进行说明,而进程区域用2中方式排列,单链表和红黑树方式,mm_rb指向红黑树的root节点全局页目录,指向页目录 |
pgd | swapper_pg_dir全局页目录,指向页目录 |
mm_users | 表示使用该内存管理结构体的处理器数量 |
mm_count | 用于mm_struct的计数,mm_user=mm_count+1 |
mmap_sem | 读写信号量变量 |
page_table_lock | 用于保护页表或计数器值的自旋锁变量 |
mmlist | 系统内所有mm_struct连接到双向链表,其第一个节点通过INIT_MM()进行初始化 |
cpu_vm_mask | 以相同内CPU数量位cpumask_t类型的cpu_vm_mask |
下面我们接着看以下pmd_clear函数
#define pmd_clear(pmdp) \
do { \
pmdp[0] = __pmd(0); \
pmdp[1] = __pmd(0); \
clean_pmd_entry(pmdp); \
} while (0)
传递给pmd_clear的参数pmdp为2M单位,并且将pmdp分为2个,并初始化为0,之后变更了页目录值,因此调用clean_pmd_entry函数
static inline void clean_pmd_entry(void *pmd)
{
const unsigned int __tlb_flag = __cpu_tlb_flags;
tlb_op(TLB_DCLEAN, "c7, c10, 1 @ flush_pmd", pmd);
tlb_l2_op(TLB_L2CLEAN_FR, "c15, c9, 1 @ L2 flush_pmd", pmd);
}
在代码中清空对应于虚拟地址pmd的MMU数据缓存,总而言之,prepare_page_table函数的作用是将页目录项的pmd段初始化为0,其对应的关系如下图所示
那么从图中可以看出prepare_page_table完成了以下的工作
- 1 对虚拟地址0到MODULES_VADDR(0xc0000000以下8MB或16MB的地址)的一级页目录项进行清空
- 2 对MODULES_VADDR到PAGE_OFFSET(0xc0000000)的一级页目录项进行清空
- 3 对lowmem顶端到VMALLOC_START的一级页目录项进行清空
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。