您的位置:首页 > 运维架构 > Linux

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映射一个地址。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息