Linux高端内存管理之永久内核映射
2023-09-11 14:20:31 时间
inux高端内存管理之永久内核映射 与直接映射的物理内存末端、高端内存的始端所对应的线性地址存放在high_memory变量中,在x86体系结构上,高于896MB的所有物理内存的范围大都是高端内存,它并不会永久地或自动地映射到内核地址空间,尽管x86处理器能够寻址物理RAM的范围达到4GB(启用PAE可以寻址到64GB)。一旦这些页被分配,就必须in射到内核的逻辑地址空间上。在x86上,高端内存中的页被映射到3GB-4GB。
内核可以采用三种不同的机制将页框映射到高端内存;分别叫做永久内核映射、临时内核映射以及非连续内存分配。在这里,只总结前两种技术,第三种技术将在后面总结。
建立永久内核映射可能阻塞当前进程;这发生在空闲页表项不存在时,也就是在高端内存上没有页表项可以用作页框的“窗口”时。因此,永久内核映射不能用于中断处理程序和可延迟函数。相反,建立临时内核映射绝不会要求阻塞当前进程;不过,他的缺点是只有很少的临时内核映射可以同时建立起来。
使用临时内核映射的内核控制路径必须保证当前没有其他的内核控制路径在使用同样地映射。这意味着内核控制路径永远不能被阻塞,后者其他内核控制路径有可能使用同一个窗口来映射其他的高端内存页。
永久内存映射
永久内核映射允许内核建立高端页框到内核地址空间的长期映射。他们使用住内核页表中一个专门的页表,其地址存放在变量pkmap_page_table中,这在前面的页表机制管理区初始化中已经介绍过了。页表中的表项数由LAST_PKMAP宏产生。因此,内核一次最多访问2MB或4MB的高端内存。
/*这里由定义可以看出永久内存映射为固定映射下面的4M空间*/
#define PKMAP_BASE ((FIXADDR_BOOT_START - PAGE_SIZE * (LAST_PKMAP + 1)) \
PMD_MASK)
该页表映射的线性地址从PKMAP_BASE开始。pkmap_count数组包含LAST_PKMAP个计数器,pkmap_page_table页表中的每一项都有一个。
高端映射区逻辑页面的分配结构用分配表(pkmap_count)来描述,它有1024项,对应于映射区内不同的逻辑页面。当分配项的值等于0时为自由项,等于1时为缓冲项,大于1时为映射项。映射页面的分配基于分配表的扫描,当所有的自由项都用完时,系统将清除所有的缓冲项,如果连缓冲项都用完时,系统将进入等待状态。
高端映射区逻辑页面的分配结构用分配表(pkmap_count)来描述,它有1024项, 对应于映射区内不同的逻辑页面。当分配项的值等于零时为自由项,等于1时为 缓冲项,大于1时为映射项。映射页面的分配基于分配表的扫描,当所有的自由 项都用完时,系统将清除所有的缓冲项,如果连缓冲项都用完时,系 统将进入等待状态。 static int pkmap_count[LAST_PKMAP]; /*last_pkmap_nr:记录上次被分配的页表项在pkmap_page_table里的位置,初始值为0,所以第一次分配的时候last_pkmap_nr等于1*/ static unsigned int last_pkmap_nr;
为了记录高端内存页框与永久内核映射包含的线性地址之间的联系,内核使用了page_address_htable散列表。该表包含一个page_address_map数据结构,用于为高端内存中的每一个页框进行当前映射。而该数据结构还包含一个指向页描述符的指针和分配给该页框的线性地址。
static struct page_address_slot { struct list_head lh; /* List of page_address_maps */ spinlock_t lock; /* Protect this buckets list */ } ____cacheline_aligned_in_smp page_address_htable[1 PA_HASH_ORDER]; * Describes one page- virtual association struct page_address_map { struct page *page; void *virtual; struct list_head list; page_address()函数返回页框对应的线性地址
spin_lock_irqsave( pas- lock, flags); if (!list_empty( pas- lh)) {/*如果对应的链表不空, 该链表中存放的是page_address_map结构*/ struct page_address_map *pam; /*对每个链表中的元素*/ list_for_each_entry(pam, pas- lh, list) { if (pam- page == page) { ret = pam- virtual;/*返回线性地址*/ goto done; done: spin_unlock_irqrestore( pas- lock, flags); return ret;
/*把页框的物理地址插入到pkmap_page_table的 一个项中并在page_address_htable散列表中加入一个 vaddr = map_new_virtual(page); pkmap_count[PKMAP_NR(vaddr)]++;/*分配计数加一,此时流程都正确应该是2了*/ BUG_ON(pkmap_count[PKMAP_NR(vaddr)] unlock_kmap(); return (void*) vaddr;/*返回地址*/ static inline unsigned long map_new_virtual(struct page *page) unsigned long vaddr; int count; start: count = LAST_PKMAP; /* Find an empty entry */ for (;;) { /*加1,防止越界*/ last_pkmap_nr = (last_pkmap_nr + 1) LAST_PKMAP_MASK; 接下来判断什么时候last_pkmap_nr等于0,等于0就表示1023(LAST_PKMAP(1024)-1)个页表项已经被分配了 ,这时候就需要调用flush_all_zero_pkmaps()函数,把所有pkmap_count[] 计数为1的页表项在TLB里面的entry给flush掉 ,并重置为0,这就表示该页表项又可以用了,可能会有疑惑为什么不在把pkmap_count置为1的时候也 就是解除映射的同时把TLB也flush呢? 个人感觉有可能是为了效率的问题吧,毕竟等到不够的时候再刷新,效率要好点吧。 if (!last_pkmap_nr) { flush_all_zero_pkmaps(); count = LAST_PKMAP; if (!pkmap_count[last_pkmap_nr]) break; /* Found a usable entry */ if (--count) continue; * Sleep for somebody else to unmap their entries DECLARE_WAITQUEUE(wait, current); __set_current_state(TASK_UNINTERRUPTIBLE); add_wait_queue( pkmap_map_wait, wait); unlock_kmap(); schedule(); remove_wait_queue( pkmap_map_wait, wait); lock_kmap(); /* Somebody else might have mapped it while we slept */ if (page_address(page)) return (unsigned long)page_address(page); /* Re-start */ goto start; /*返回这个页表项对应的线性地址vaddr.*/ vaddr = PKMAP_ADDR(last_pkmap_nr); set_pte_at(mm, addr, ptep, pte)函数在NON-PAE i386上的实现其实很简单,其实就等同于下面的代码: static inline void native_set_pte(pte_t *ptep , pte_t pte) *ptep = pte; */ set_pte_at( init_mm, vaddr,/*设置页表项*/ (pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot)); /*接下来把pkmap_count[last_pkmap_nr]置为1,1不是表示不可用吗, 既然映射已经建立好了,应该赋值为2呀,其实这个操作 是在他的上层函数kmap_high里面完成的(pkmap_count[PKMAP_NR(vaddr)]++).*/ pkmap_count[last_pkmap_nr] = 1; /*到此为止,整个映射就完成了,再把page和对应的线性地址 加入到page_address_htable哈希链表里面就可以了*/ set_page_address(page, (void *)vaddr); return vaddr; kunmap()函数撤销先前由kmap()建立的永久内核映射 如果页确实在高端内存中,则调用kunmap_high()函数
* If ARCH_NEEDS_KMAP_HIGH_GET is not defined then this may be called * only from user context. void kunmap_high(struct page *page) unsigned long vaddr; unsigned long nr; unsigned long flags; int need_wakeup; lock_kmap_any(flags); vaddr = (unsigned long)page_address(page); BUG_ON(!vaddr); nr = PKMAP_NR(vaddr);/*永久内存区域开始的第几个页面*/ * A count must never go down to zero * without a TLB flush! need_wakeup = 0; switch (--pkmap_count[nr]) {/*减小这个值,因为在映射的时候对其进行了加2*/ case 0: BUG(); case 1: * Avoid an unnecessary wake_up() function call. * The common case is pkmap_count[] == 1, but * no waiters. * The tasks queued in the wait-queue are guarded * by both the lock in the wait-queue-head and by * the kmap_lock. As the kmap_lock is held here, * no need for the wait-queue-heads lock. Simply * test if the queue is empty. need_wakeup = waitqueue_active( pkmap_map_wait); unlock_kmap_any(flags); /* do wake-up, if needed, race-free outside of the spin lock */ if (need_wakeup) wake_up( pkmap_map_wait); 最新内容请见作者的GitHub页:http://qaseven.github.io/
高端映射区逻辑页面的分配结构用分配表(pkmap_count)来描述,它有1024项, 对应于映射区内不同的逻辑页面。当分配项的值等于零时为自由项,等于1时为 缓冲项,大于1时为映射项。映射页面的分配基于分配表的扫描,当所有的自由 项都用完时,系统将清除所有的缓冲项,如果连缓冲项都用完时,系 统将进入等待状态。 static int pkmap_count[LAST_PKMAP]; /*last_pkmap_nr:记录上次被分配的页表项在pkmap_page_table里的位置,初始值为0,所以第一次分配的时候last_pkmap_nr等于1*/ static unsigned int last_pkmap_nr;
为了记录高端内存页框与永久内核映射包含的线性地址之间的联系,内核使用了page_address_htable散列表。该表包含一个page_address_map数据结构,用于为高端内存中的每一个页框进行当前映射。而该数据结构还包含一个指向页描述符的指针和分配给该页框的线性地址。
static struct page_address_slot { struct list_head lh; /* List of page_address_maps */ spinlock_t lock; /* Protect this buckets list */ } ____cacheline_aligned_in_smp page_address_htable[1 PA_HASH_ORDER]; * Describes one page- virtual association struct page_address_map { struct page *page; void *virtual; struct list_head list; page_address()函数返回页框对应的线性地址
spin_lock_irqsave( pas- lock, flags); if (!list_empty( pas- lh)) {/*如果对应的链表不空, 该链表中存放的是page_address_map结构*/ struct page_address_map *pam; /*对每个链表中的元素*/ list_for_each_entry(pam, pas- lh, list) { if (pam- page == page) { ret = pam- virtual;/*返回线性地址*/ goto done; done: spin_unlock_irqrestore( pas- lock, flags); return ret;
/*把页框的物理地址插入到pkmap_page_table的 一个项中并在page_address_htable散列表中加入一个 vaddr = map_new_virtual(page); pkmap_count[PKMAP_NR(vaddr)]++;/*分配计数加一,此时流程都正确应该是2了*/ BUG_ON(pkmap_count[PKMAP_NR(vaddr)] unlock_kmap(); return (void*) vaddr;/*返回地址*/ static inline unsigned long map_new_virtual(struct page *page) unsigned long vaddr; int count; start: count = LAST_PKMAP; /* Find an empty entry */ for (;;) { /*加1,防止越界*/ last_pkmap_nr = (last_pkmap_nr + 1) LAST_PKMAP_MASK; 接下来判断什么时候last_pkmap_nr等于0,等于0就表示1023(LAST_PKMAP(1024)-1)个页表项已经被分配了 ,这时候就需要调用flush_all_zero_pkmaps()函数,把所有pkmap_count[] 计数为1的页表项在TLB里面的entry给flush掉 ,并重置为0,这就表示该页表项又可以用了,可能会有疑惑为什么不在把pkmap_count置为1的时候也 就是解除映射的同时把TLB也flush呢? 个人感觉有可能是为了效率的问题吧,毕竟等到不够的时候再刷新,效率要好点吧。 if (!last_pkmap_nr) { flush_all_zero_pkmaps(); count = LAST_PKMAP; if (!pkmap_count[last_pkmap_nr]) break; /* Found a usable entry */ if (--count) continue; * Sleep for somebody else to unmap their entries DECLARE_WAITQUEUE(wait, current); __set_current_state(TASK_UNINTERRUPTIBLE); add_wait_queue( pkmap_map_wait, wait); unlock_kmap(); schedule(); remove_wait_queue( pkmap_map_wait, wait); lock_kmap(); /* Somebody else might have mapped it while we slept */ if (page_address(page)) return (unsigned long)page_address(page); /* Re-start */ goto start; /*返回这个页表项对应的线性地址vaddr.*/ vaddr = PKMAP_ADDR(last_pkmap_nr); set_pte_at(mm, addr, ptep, pte)函数在NON-PAE i386上的实现其实很简单,其实就等同于下面的代码: static inline void native_set_pte(pte_t *ptep , pte_t pte) *ptep = pte; */ set_pte_at( init_mm, vaddr,/*设置页表项*/ (pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot)); /*接下来把pkmap_count[last_pkmap_nr]置为1,1不是表示不可用吗, 既然映射已经建立好了,应该赋值为2呀,其实这个操作 是在他的上层函数kmap_high里面完成的(pkmap_count[PKMAP_NR(vaddr)]++).*/ pkmap_count[last_pkmap_nr] = 1; /*到此为止,整个映射就完成了,再把page和对应的线性地址 加入到page_address_htable哈希链表里面就可以了*/ set_page_address(page, (void *)vaddr); return vaddr; kunmap()函数撤销先前由kmap()建立的永久内核映射 如果页确实在高端内存中,则调用kunmap_high()函数
* If ARCH_NEEDS_KMAP_HIGH_GET is not defined then this may be called * only from user context. void kunmap_high(struct page *page) unsigned long vaddr; unsigned long nr; unsigned long flags; int need_wakeup; lock_kmap_any(flags); vaddr = (unsigned long)page_address(page); BUG_ON(!vaddr); nr = PKMAP_NR(vaddr);/*永久内存区域开始的第几个页面*/ * A count must never go down to zero * without a TLB flush! need_wakeup = 0; switch (--pkmap_count[nr]) {/*减小这个值,因为在映射的时候对其进行了加2*/ case 0: BUG(); case 1: * Avoid an unnecessary wake_up() function call. * The common case is pkmap_count[] == 1, but * no waiters. * The tasks queued in the wait-queue are guarded * by both the lock in the wait-queue-head and by * the kmap_lock. As the kmap_lock is held here, * no need for the wait-queue-heads lock. Simply * test if the queue is empty. need_wakeup = waitqueue_active( pkmap_map_wait); unlock_kmap_any(flags); /* do wake-up, if needed, race-free outside of the spin lock */ if (need_wakeup) wake_up( pkmap_map_wait); 最新内容请见作者的GitHub页:http://qaseven.github.io/
相关文章
- 重新点亮linux 命令树————内存与文件系统的查看[二十七]
- Linux下查看内存使用情况方法总结
- linux清理cache缓存以释放内存
- Linux堆内存管理深入分析(上)
- Linux 0.11 - 管理内存前先划分出三个边界值-12
- 【Linux 内核 内存管理】物理分配页 ⑤ ( get_page_from_freelist 快速路径调用函数源码分析 | 遍历备用区域列表 | 启用 cpuset 检查判定 | 判定脏页数量 )
- 【Linux 内核 内存管理】memblock 分配器编程接口 ⑤ ( memblock_free 函数 | memblock_remove_range 函数 )
- 【Linux 内核 内存管理】memblock 分配器编程接口 ③ ( memblock_remove 函数 | memblock_remove_range 函数 )
- 【Linux 内核 内存管理】memblock 分配器编程接口 ② ( memblock_add_range 函数分析 | memblock_insert_region 函数分析 )
- 【Linux 内核 内存管理】memblock 分配器编程接口 ① ( memblock 分配器编程接口简介 | memblock_add 函数原型分析 | memblock_add 函数源码 )
- 【Linux 内核 内存管理】memblock 分配器 ② ( memblock_type 内存块类型 | memblock_type 结构体成员分析 )
- 【Linux 内核 内存管理】引导内存分配器 bootmem ③ ( bootmem 引导内存分配器算法 | 低端内存映射 | 内存记录位图 | 最先适配算法 | 内存分配记录 | 内存操作函数 )
- 【Linux 内核 内存管理】引导内存分配器 bootmem ② ( bootmem_data 结构体源码分析 | bootmem_data 与内存节点 pglist_data 的关联 )
- 【Linux 内核 内存管理】内存管理系统调用 ⑤ ( 代码示例 | 多进程共享 mmap 内存映射示例 )
- 【Linux 内核 内存管理】内存管理系统调用 ③ ( mmap 创建内存映射原理 | 分配虚拟内存页 | 物理地址与虚拟地址进行映射 | 并分配物理内存页 | mmap 库函数与内核系统调用函数 )
- 【Linux 内核 内存管理】内存映射相关数据结构 ① ( vm_area_struct 结构体 | task_struct、mm_struct、vm_area_struct 3 个结构体之间的关系)
- 【Linux 内核 内存管理】内存映射原理 ② ( 内存映射概念 | 文件映射 | 匿名映射 | 内存映射原理 | 分配虚拟内存页 | 产生缺页异常 | 分配物理内存页 | 共享内存 | 进程内存 )
- 【Linux 内核 内存管理】虚拟地址空间布局架构 ⑥ ( mm_struct 结构体源码 | vm_area_struct 结构体源码 )
- 【Linux 内核 内存管理】虚拟地址空间布局架构 ⑤ ( Linux 内核中对 “ 虚拟地址空间 “ 的描述 | task_struct 结构体源码 )
- 【Linux 内核 内存管理】优化内存屏障 ② ( 内存屏障 | 编译器屏障 | 处理器内存屏障 | 内存映射 I/O 写屏障 )
- 【Linux 内核 内存管理】RCU 机制 ① ( RCU 机制简介 | RCU 机制的优势与弊端 | RCU 机制的链表应用场景 )
- 【Linux 内核】NUMA 非一致内存访问结构 ( NUMA 概念介绍 | NUMA 架构优势分析 | SMP、NUMA、MPP 架构 )
- 【Linux 内核】Linux 内核源码几个重要的入口源文件及函数介绍 ( 系统初始化 | 内存管理 | 虚拟文件系统 | 网络管理 )
- L74.linux命令每日一练 -- 第十章 Linux网络管理命令 -- nmap和tcpdump
- L35.linux命令每日一练 -- 第五章 Linux信息显示与搜索文件命令 -- which和whereis
- linux top命令及结果详解 top -p 查看Linux程序运行进程
- 74:应急响应-win&linux分析后门&勒索病毒&攻击 ==》暴力破解攻击成功的在4624的eventID里!PChunter可查看非系统的可疑启动项/服务/定时任务,还是很直观的!Linux下gscan也不错,EDR可参考。
- Linux:内存
- Linux基础笔记4 | 必备基础命令