Linux内核之高端内存
2017-05-23 23:35
169 查看
物理地址空间布局
CPU所访问的都是虚拟内存地址。通常32位Linux内核地址空间划分0~3G为用户空间,3~4G为内核空间。Linux系统在初始化时,会根据实际的物理内存的大小,为每个物理页面创建一个page对象,所有的page对象构成一个mem_map数组。
进一步,针对不同的用途,Linux内核将所有的物理页面划分到3类内存管理区中,如图,分别为ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。
ZONE_DMA的范围是0~16M,该区域的物理页面专门供I/O设备的DMA使用。之所以需要单独管理DMA的物理页面,是因为DMA使用物理地址访问内存,不经过MMU,并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于DMA。
ZONE_NORMAL的范围是16M~896M,该区域的物理页面是内核能够直接使用的。
ZONE_HIGHMEM的范围是896M~结束,该区域即为高端内存,内核不能直接使用。
为什么有高端内存的概念?
当内核模块代码或线程访问内存时,代码中的内存地址都为逻辑地址,而对应到真正的物理内存地址,需要地址一对一的映射,假设简单的地址映射关系,那么内核逻辑地址空间访问为0xc0000000 ~ 0xffffffff,那么只能访问1G物理内存。若机器中安装8G物理内存,那么内核就只能访问前1G物理内存,后面7G物理内存将会无法访问。显然不能将内核地址空间0xc0000000 ~ 0xfffffff全部用来简单的地址映射。因此32bit OS将内核地址空间划分三部分:ZONE_DMA、ZONE_NORMAL和 ZONE_HIGHMEM。ZONE_HIGHMEM即为高端内存.
只有1G的线性地址空间,如何使用大于1G的物理内存?这就是高端内存的由来。
内核不能把1G的线性地址全部用来直接内存地址映射,而是需要保留128M的地址空间用来映射896M以上的内存地址。
举个简单的例子:
从公司下班回家需要乘坐328公交,公交车有30个固定的位置以及可以容纳20人站立的过道,公交途径很多站,假设公交一开始三十个位置已经固定有人,且中途不下车,在各个站台,过道的乘客上上下下。
328公交只能载客50人吗?显然不是,到达终点站时,售票员发现售出了150张票。
如何将50人载客量的公交实际载客150人?那就是通过在不同的站台上上下下,但任一时刻,公交最多容纳50人。
与之类比,内核空间中896M对应公交的30个固定位置,用作直接映射。高端内存的128M对应让乘客站着的空间,用作临时映射,这样可以通过128M的线性地址空间,访问超过1G的物理内存。
linux 高端内存的划分
内核将高端内存划分为3部分:VMALLOC_START~VMALLOC_END、KMAP_BASE~FIXADDR_START和FIXADDR_START~4G。
对 于高端内存,可以通过 alloc_page() 或者其它函数获得对应的 page,但是要想访问实际物理内存,还得把 page 转为线性地址才行(为什么?想想 MMU 是如何访问物理内存的),也就是说,我们需要为高端内存对应的 page 找一个线性空间,这个过程称为高端内存映射。
对应高端内存的3部分,高端内存映射有三种方式:
映射到”内核动态映射空间”(noncontiguous memory allocation)
这种方式很简单,因为通过 vmalloc() ,在”内核动态映射空间”申请内存的时候,就可能从高端内存获得页面(参看 vmalloc 的实现),因此说高端内存有可能映射到”内核动态映射空间”中。
持久内核映射(permanent kernel mapping)
如果是通过 alloc_page() 获得了高端内存对应的 page,如何给它找个线性空间?
内核专门为此留出一块线性空间,从 PKMAP_BASE 到 FIXADDR_START ,用于映射高端内存。在 2.6内核上,这个地址范围是 4G-8M 到 4G-4M 之间。这个空间起叫”内核永久映射空间”或者”永久内核映射空间”。这个空间和其它空间使用同样的页目录表,对于内核来说,就是 swapper_pg_dir,对普通进程来说,通过 CR3 寄存器指向。通常情况下,这个空间是 4M 大小,因此仅仅需要一个页表即可,内核通过来 pkmap_page_table 寻找这个页表。通过 kmap(),可以把一个 page 映射到这个空间来。由于这个空间是 4M 大小,最多能同时映射 1024 个 page。因此,对于不使用的的 page,及应该时从这个空间释放掉(也就是解除映射关系),通过 kunmap() ,可以把一个 page 对应的线性地址从这个空间释放出来。
临时映射(temporary kernel mapping)
内核在 FIXADDR_START 到 FIXADDR_TOP 之间保留了一些线性空间用于特殊需求。这个空间称为”固定映射空间”在这个空间中,有一部分用于高端内存的临时映射。
非连续内存的管理
非连续内存的线性地址空间是从VMALLOC_START~VMALLOC_END,共128MB大小。当内核需要用vmalloc类的函数进行非连续内存分配时,就会申请一个vm_struct结构来描述对应的vmalloc区,若分配多个vmalloc的内存区,那么相邻两个vmalloc区之间的间隔大小至少为4KB,即至少是一个页框大小。vmalloc是内核中使用到的内存分配函数,一般用来分配大块内存,
这个函数得到的是连续的虚拟地址,物理上地址不连续。
连续物理内存的分配并不是必要的。对于大部分
4000
DMA操作,我们的确需要连续的物理内存;但是对于某些分配内存情况:比如,模块加载,设备和声音驱动程序中,可以在内核源码中关键字vmalloc查找,对vmalloc的使用有个感性认识。
内核中描述非连续区的数据结构是struct vm_struct:
struct vm_struct { struct vm_struct *next; //指向下一个vm_struct区,所有非连续区组成一个单链表 void *addr; //代表每个内存区的起始地址,即指向申请的内存区的第一个内存单元(线性地址) unsigned long size; //当前所申请的内存区大小加4KB(安全区) unsigned long flags; //标识内存区类型 struct page **pages; //指向nr_pages页描述符指针数组的指针 unsigned int nr_pages; //所申请的内存区大小对应的页框数 phys_addr_t phys_addr; //该字段一般为0,除非内存已经被申请用作映射一个硬件设备的I/O共享内存 const void *caller; //当前调用vmalloc类的函数的返回地址 };
1分配非连续的内存区
分配函数主要是vmalloc(),vmap(),vmalloc()会去调用__vmalloc_node_range()函数:
void *__vmalloc_node_range(unsigned long size, unsigned long align, unsigned long start, unsigned long end, gfp_t gfp_mask, pgprot_t prot, unsigned long vm_flags, int node, const void *caller) { struct vm_struct *area; void *addr; unsigned long real_size = size; //size要对其为4K的整数倍,因为非连续内存区域是将各个物理页进行映射 size = PAGE_ALIGN(size); if (!size || (size >> PAGE_SHIFT) > totalram_pages) goto fail; //找到一块空闲的线性地址区域,用来映射该非连续内存 area = __get_vm_area_node(size, align, VM_ALLOC | VM_UNINITIALIZED | vm_flags, start, end, node, gfp_mask, caller); if (!area) goto fail; addr = __vmalloc_area_node(area, gfp_mask, prot, node); if (!addr) return NULL; /* * In this function, newly allocated vm_struct has VM_UNINITIALIZED * flag. It means that vm_struct is not fully initialized. * Now, it is fully initialized, so remove this flag here. */ clear_vm_uninitialized_flag(area); /* * A ref_count = 2 is needed because vm_struct allocated in * __get_vm_area_node() contains a reference to the virtual address of * the vmalloc'ed block. / /*一个内存块分配的通知*/ kmemleak_alloc(addr, real_size, 2, gfp_mask); return addr; fail: warn_alloc(gfp_mask, "vmalloc: allocation failure: %lu bytes", real_size); return NULL; }
分配物理页面 并映射
__vmalloc_area_node:
static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask, pgprot_t prot, int node) { struct page **pages; unsigned int nr_pages, array_size, i; const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO; const gfp_t alloc_mask = gfp_mask | __GFP_NOWARN; // 计算想要分配的物理页数 nr_pages = get_vm_area_size(area) >> PAGE_SHIFT; array_size = (nr_pages * sizeof(struct page *)); area->nr_pages = nr_pages; /* Please note that the recursion is strictly bounded. */ //array_size大于PAGE_SIZE的话,就需要递归调用__vmalloc_node申请内存, //否则直接调用kmalloc_node申请内存 if (array_size > PAGE_SIZE) { pages = __vmalloc_node(array_size, 1, nested_gfp|__GFP_HIGHMEM, PAGE_KERNEL, node, area->caller); } else { pages = kmalloc_node(array_size, nested_gfp, node); } area->pages = pages; if (!area->pages) { remove_vm_area(area->addr); kfree(area); return NULL; } //为非连续内存进行页面的分配,每次分配一个页面,将其页框指针记录在pages数组中,可以看到物理内存地址不是连续的 for (i = 0; i < area->nr_pages; i++) { struct page *page; if (node == NUMA_NO_NODE) page = alloc_page(alloc_mask); else page = alloc_pages_node(node, alloc_mask, 0); if (unlikely(!page)) { /* Successfully allocated i pages, free them in __vunmap() */ area->nr_pages = i; goto fail; } area->pages[i] = page; if (gfpflags_allow_blocking(gfp_mask)) cond_resched(); } //将虚拟地址跟每个物理页面建立映射 if (map_vm_area(area, prot, pages)) goto fail; return area->addr; fail: warn_alloc(gfp_mask, "vmalloc: allocation failure, allocated %ld of %ld bytes", (area->nr_pages*PAGE_SIZE), area->size); vfree(area->addr); return NULL; }
持久内核映射
从 PKMAP_BASE 到 FIXADDR_START,为内核的持久映射空间。通过kmap函数实现,将高端内存长期映射到内核地址空间中。
kmap的内核实现:
void *kmap(struct page *page) { BUG_ON(in_interrupt()); if (!PageHighMem(page)) return page_address(page); return kmap_high(page); }
如果给定的page不是高端物理页面,直接通过page_address返回该页面的虚拟地址
否则调用kmap_high建立高端映射.
kmap_high:
void *kmap_high(struct page *page) { unsigned long vaddr; /* * For highmem pages, we can't trust "virtual" until * after we have the lock. */ lock_kmap(); vaddr = (unsigned long)page_address(page); if (!vaddr) vaddr = map_new_virtual(page); pkmap_count[PKMAP_NR(vaddr)]++; BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2); unlock_kmap(); return (void*) vaddr; }
在kmap_high函数中先调用page_address检查该page是否已经被映射。如果没有映射调用map_new_virtual(page)处理。
pkmap_count是一个计数数组,标示该页被引用的次数。 每一个持久地址映射项都对应一个计数.
对于这个计数,我们主要区分三种情况:
1. count= 0, 相应的页表项还没有映射到高端物理内存页框,可以使用它
2. count = 1,相应的页表项没有映射到高端物理内存页框,但是现在不能使用它,因为自从上次使用过后,相应的TLB项还没有刷新。
3. count > 1,相应的页表项已经映射到高端物理内存页框,使用者的数目是n - 1
步骤:通过kmap将传入的page*映射到虚拟内存时分以下几步:1.在内核地址空间(持久映射区)分配一个页;2.建立传入的物理内存页和虚拟地址之间的映射;3.统计内核地址空间中哪些页被引用。
map_new_virtual:
static inline unsigned long map_new_virtual(struct page *page) { unsigned long vaddr; int count; unsigned int last_pkmap_nr; unsigned int color = get_pkmap_color(page); start: count = get_pkmap_entries_count(color); /* Find an empty entry */ for (;;) { last_pkmap_nr = get_next_pkmap_nr(color); if (no_more_pkmaps(last_pkmap_nr, color)) { flush_all_zero_pkmaps(); count = get_pkmap_entries_count(color); } 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); wait_queue_head_t *pkmap_map_wait = get_pkmap_wait_queue_head(color); __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 = PKMAP_ADDR(last_pkmap_nr); set_pte_at(&init_mm, vaddr, &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot)); pkmap_count[last_pkmap_nr] = 1; set_page_address(page, (void *)vaddr); return vaddr; }
1.从上一次使用的位置last_pkmap_nr开始反向扫描pkmap_count数组,直到找到一个空闲位置,如果没有空闲位置,该函数进入睡眠状态,直到内核的另一部分执行解除映射操作。
2.调用set_pte_at()修改内核页表,将该页映射到指定位置,但尚未更新TLB。
3.pkmap_count[last_pkmap_nr] = 1;新位置的使用计数器设置为1,表示该分页已分配,但是无法使用。因为TLB项未更新。
4.调用set_page_address()将该页添加到内核映射的数据结构。
set_page_address:
/** * set_page_address - set a page's virtual address * @page: &struct page to set * @virtual: virtual address to use */ void set_page_address(struct page *page, void *virtual) { unsigned long flags; struct page_address_slot *pas; struct page_address_map *pam; BUG_ON(!PageHighMem(page)); pas = page_slot(page); if (virtual) { /* Add */ pam = &page_address_maps[PKMAP_NR((unsigned long)virtual)]; pam->page = page; pam->virtual = virtual; spin_lock_irqsave(&pas->lock, flags); list_add_tail(&pam->list, &pas->lh); spin_unlock_irqrestore(&pas->lock, flags); } else { /* Remove */ spin_lock_irqsave(&pas->lock, flags); list_for_each_entry(pam, &pas->lh, list) { if (pam->page == page) { list_del(&pam->list); spin_unlock_irqrestore(&pas->lock, flags); goto done; } } spin_unlock_irqrestore(&pas->lock, flags); } done: < b4fa span class="hljs-keyword">return; }
该函数为page指针和虚拟地址建立一个page_address_map结构来映射,并将结构添加到page_address_maps全局hash链表中。
总结步骤:
1.当调用kmap函数,传入一个物理页帧page的指针时,会返回该page关联的虚拟地址。kmap函数中先判断该page是低端内存还是高端内存,如果是低端内存,则直接调用page_address函数,通过直接映射关系,计算出该page关联的虚拟地址;如果传入kmap函数的page对应的是高端内存的物理页帧,则调用kmap_high函数处理。
2.进入在kmap_high函数中,说明入参page对应高端内存的物理页帧,仍然要先调用page_address判断该物理页是否已经关联了虚拟地址。如果已经关联则将对应的虚拟地址在pkmap_count中的项+1,返回该page对应的虚拟地址。如果没有关联,则调用map_new_virtual函数,给该page建立关联的物理地址。
3.在map_new_virtual函数中,先通过扫描pkmap_count中的项为该page找到未用的虚拟地址,然后修改页表,便于通过虚拟地址找到物理地址。然后调用set_page_address函数为page和虚拟地址建立映射结构page_address_map,并添加到上图所示的结构中。
临时映射
临时内核映射实现简单,可以用在中断处理程序和可延迟函数的内部(这些函数不能被阻塞),因为临时内核映射从来不阻塞当前进程,因为它被设计成是原子的。对比永久内核映射,发现如果页框暂时没有空闲的虚拟地址可以映射,那么永久内核映射将要被阻塞。建立临时内核映射禁用内核抢占,这是必须的,因为映射对于每个处理器都是独特的,如果没有禁用抢占,那么哪个任务在哪个CPU上运行是不确定的。
临时映射通过kmap_atomic函数实现:
void *kmap_atomic(struct page *page) { int idx, cpu_idx; unsigned long vaddr; /** *原子映射是基于每个cpu的,因此在当前cpu上应用抢占,直到unmap的时候才 *开启,这样不会导致原子映射的重入了, */ preempt_disable(); pagefault_disable(); if (!PageHighMem(page)) return page_address(page); //递增一个每cpu变量,返回递增后的结果 cpu_idx = kmap_atomic_idx_push(); /* *kernel可以在多个cpu上同时运行不同的task,他们共同使用一个内存地址空间 *也就是说,内存空间对于多个cpu看到的是同一个,该函数使用的是地址空间中顶部的 *一小段地址空间,也就是临时映射区,内核逻辑将这一小段地址空间分成若干各节 *每一节的大小是一个页面的大小,可以映射一个页面,根据公用地址空间的原理 *所有的cpu共同使用这些节,因此如何能保证N个cpu调用此函数不会将page映射一个地址呢, *这就是这个数学公式所起到的作用 */ idx = cpu_idx + KM_TYPE_NR * smp_processor_id(); //固定映射的线性地址转化成虚拟地址 vaddr = FIXMAP_ADDR(idx); //设置页表 set_pte_at(&init_mm, vaddr, fixmap_page_table + idx, mk_pte(page, kmap_prot)); return (void *)vaddr; } EXPORT_SYMBOL(kmap_atomic);
调用kmap_atomic的时候,有一个参数是的枚举类型km_type,不同的cpu得到的vaddr不一样,保证N个cpu调用此函数不会将page映射一个地址。
相关文章推荐
- [置顶] linux内核启动2-setup_arch中的内存初始化(目前分析高端内存)
- Linux内核高端内存
- Linux内核高端内存管理
- 【转载】linux内核笔记之高端内存映射
- Linux内核高端内存
- Linux内核-内存管理之高端内存
- Linux内核高端内存
- Linux内核高端内存
- Linux内核高端内存
- Linux内核高端内存
- Linux内核高端内存管理
- Linux内核高端内存
- Linux内核高端内存
- linux内核高端内存管理之固定内存区与映射
- Linux内核高端内存
- 浅析linux内核内存管理之高端内存(上)
- linux内存管理--linux内核高端内存
- Linux内核高端内存
- Linux内核高端内存
- Linux内核高端内存