进程与内存3-内存管理(解析并获取高低端内存)

时间:2022-11-26 14:26:35

ldd3的一段话:

高效的块驱动对于性能是重要的 -- 不只是为在用户应用程序的明确的读和写.现代的有虚拟内存的系统将不需要的数据移向(希望地)二级存储中, 它常常是一个磁盘驱动器. 块驱动是核心内存和二级存储之间的导管; 因此, 它们可组成虚拟内存子系统的一部分. 虽然可能编写一个块驱动不必知道 struct page和其他重要的内存概念, 任何需要编写一个高性能驱动的人必须使用 15 章所涉及的内容.

15章就是内存映射和 DMA,所以我选择了先熟悉一下这些知识,再去搞驱动。

简要:

1.      非高端内存获取,包括DMA、NOMAL和保留的页框池。

2.      高端内存获取。以及pkmap fixmap 非连续内存区的概念。

3.      简单说说malloc、kmalloc和vmalloc的区别。

先理解一些操蛋的概念

a.页:线性地址被分成以固定长度为单位的组。代表一个数据块,可以存放在内存或磁盘中。

b.页框:Ram分成固定长度的页(即物理页)。页框就是主存的一部分,因此也是一个存储区域。我们在内核编程时会遇到virt_to_page(addr)宏产生线性地址addr对应的页描述符地址。pfn_to_page(pfn)宏产生与页框号pfn对应的页描述符地址。现在对这些应该能理解了吧!

c.页表和页目录:看图说话

进程与内存3-内存管理(解析并获取高低端内存)

这个图太多人看过了,cr3是x86的东西,我们不管。

从这个图你可以看到就是二级分页。

d.页全局目录(PGD);页上级目录(PUD);页中级目录(PMD);页表(PTE)

  linux为了适应32位和64位,从2.6.11开始使用了四级分页模式,图就不贴了,对于32位系统,PUD和PMD就是0。那么就和二级分页一样了。

f. 页表项和页目录项:页表项就是存储页表的单位,是32位,它还有页面的属性等信息。页目录项类似。


Linux初始化就会建立页表,这个我之前的文章有分析过代码,当然网上也有很多。

 1. 在未启动分页机制下初始化一个可以寻址0—8M的临时内核页表,这个是最小限度的地址空间仅能内核装载到RAM和对其初始化核心数据结构。当然这不是我们现在要关心的。

2. 初始化页表: 分为低端和高端,高端又分为pkmap和fixmap。其实对应平台的还有设备寄存器的静态映射(这个在这也不提了,以前文章有分析过)。


 为什么要高端内存:

简单举个例子,假设你有2G内存,而内核只有1G不能全部做线性映射,内核就会把前896M用于RAM线性映射,后128M可以通过更改映射关系访问剩下的内存。

 从上面的话好像高端地址就用动态去建立页与页框的关系,但是这个只是一种就是pkmap,实际有的也是固定的映射。

 

低端内存:

源码不看了,简单说一下低端映射就是把对应的页框号存入页表里。当然会涉及mmu的操作。pkmap和fixmap下面说。

 现在我们看看写程序时常做的事。申请内存。先看一个获取低端内存的简单程序

 

#include <linux/init.h>

#include <linux/module.h>

#include <linux/gfp.h>

 

#include <asm/pgtable.h>

 

MODULE_LICENSE("Dual BSD/GPL");

MODULE_AUTHOR("Wang Xiao Lu\n");

MODULE_VERSION("V1.0");

 

static int __init lowmem_init(void)

{

   unsigned long *p = NULL;

    p= (unsigned long *)__get_free_page(__GFP_DMA);

   if (p != NULL)

    {

       printk(KERN_INFO "p = %lx\n", (unsigned long)p);

       free_page((unsigned long)p);

       p = NULL;

    }

 

    p= (unsigned long *)__get_free_page(__GFP_WAIT | __GFP_IO | __GFP_FS);

   if (p != NULL)

    {

       printk(KERN_INFO "p = %lx\n", (unsigned long)p);

       free_page((unsigned long)p);

       p = NULL;

    }

 

    p= (unsigned long *)__get_free_page(__GFP_HIGH);

   if (p != NULL)

    {

       printk(KERN_INFO "p = %lx\n", (unsigned long)p);

       free_page((unsigned long)p);

       p = NULL;

    }

 

    return0;

}

 

static void __exit lowmem _exit(void)

{

}

 

module_init(lowmem _init);

module_exit(lowmem _exit);

 

看看打印结果:

p = c0f70000

p = c7cb5000

p = c7cb5000

 

解释:这个是在x86下运行的,

第一个__GFP_DMA,我们知道DMA内存是在从0xc0000000开始低于16MB下,最高地址是0xc1000000。我们申请的是0xc0f70000。

第二个__GFP_WAIT| __GFP_IO | __GFP_FS,等价于GFP_KERNEL,我想大家已经知道,低端内存的获取。

第三个__GFP_HIGH,大家千万不要误解,高端内存的申请是__GFP_HIGHMEM。这个其实相当于GFP_ATOMIC。__GFP_HIGH意思是允许内核访问保留的页框池,记住是允许不是一定,所以和第二个地址相同。现在只要明白保留的页框池这个概念就可以了。简单解释linux为了保证在中断或执行临界区的原子内存分配请求,保留了一个页框池,只有在内存不足时使用。这个池包含DMA和NORMAL内存区,是它们按一定比例分配的。

 

下面我们再来看看高端内存:

这是arm平台linux-3.2.36,我以前写的《linux-3.2.36内核启动》里面分析的

               if(__va(bank->start) >= vmalloc_min ||

                   __va(bank->start) < (void *) PAGE_OFFSET)

                       highmem = 1;

               bank->highmem = highmem;

把一个bank表示为highmem;

上面的条件:

大于等于vmalloc_min的好理解,小于PAGE_OFFSET是pkmap。

 

大家应该知道有三种pkmap fixmap 非连续内存区,看下图

进程与内存3-内存管理(解析并获取高低端内存)

如果你只在x86上溜达,你可以认为这是一张神图。如果你和我一样被下放到arm平台上,那么请注意:

 

X86下定义:#define PKMAP_BASE ((FIXADDR_BOOT_START - PAGE_SIZE * (LAST_PKMAP +1))

Arm下定义:#define PKMAP_BASE             (PAGE_OFFSET - PMD_SIZE)

我的一个平台启动打印:

    vector  : 0xffff0000 - 0xffff1000   (   4kB)

    fixmap  : 0xfff00000 - 0xfffe0000   ( 896 kB)

    vmalloc : 0xc4800000 -0xf6000000   ( 792 MB)

    lowmem  : 0xc0000000 - 0xc4000000   (  64MB)

    pkmap   : 0xbfe00000 - 0xc0000000   (   2MB)

modules : 0xbf000000 -0xbfe00000  (  14 MB)

PKMAP_BASE是0xbfe00000,在PAGE_OFFSET下2M地方。

我的pc的PKMAP_BASE是0xff800000

至于linux为什么要这样我还没弄清楚。

 

pkmap初始化就是分配一个对应起始虚拟地址为PKMAP_BASE的页表pkmap_page_table。运行时把页框的物理地址插入到pkmap_page_table的一个项中并在page_address_htable散列表中加入一个元素。这个散列表记录高端内存页框与pkmap包含的线性地址之间的关系。例如page_addresss()函数如果判断页框在高端内存中,它会到根据这个散列表中到页框。这个可改的page_address_htable和pkmap_page_table体现了动态的含义。

 

下面是一位同志在arm下做的实验,它在开头并没有提到自己是在arm平台,这样大家就会对PKMAP_BASE的值产生纠纷。下面是地址,其实它分析的很好,但是没有说明这点,可能让人误解。地址在下面:

blog.csdn.net/xiaojsj111/article/details/11817587

 

我的arm板内存太少,所以只能在x86下做了。Linux-2.6.18

上面我们看到__get_free_page(),这个函数返回的是第一个被分配页框的线性地址。还有个函数时alloc_page(),这个函数是返回第一个被分配页框的页描述符的线性地址。这个地址是初始化就存在的,不会改变。

现在有个问题,如果你用__get_free_page(GFP_HIGHMEM,0)在高端内存分配一个页框,它会分配成功,但是返回NULL,因为页框的线性地址并不存在,只存在被分配的页框的页描述符的线性地址。所以我们要通过alloc_page()分配页框的页描述符的线性地址,再映射。

pkmap例子:

#include <linux/init.h>

#include <linux/module.h>

#include <linux/gfp.h>

#include <linux/highmem.h>

 

#include <asm/pgtable.h>

 

MODULE_LICENSE("Dual BSD/GPL");

MODULE_AUTHOR("Wang Xiao Lu\n");

MODULE_VERSION("V1.0");

 

static int __init highmem_init(void)

{

   struct page *p = NULL;

   unsigned long * addr = NULL;

 

    p= (struct page *)alloc_page(__GFP_HIGHMEM);

   if (p != NULL)

    {

       printk(KERN_INFO "p = %lx\n", (unsigned long)p);

       addr = (unsigned long *)kmap(p);

       if (addr != NULL)

       {

           printk(KERN_INFO "addr = %lx\n", (unsigned long)addr);

 

           kunmap(p);

       }

 

       __free_page(p);

       p = NULL;

    }

 

   return 0;

}

 

static void __exit highmem_exit(void)

{

}

 

module_init(highmem_init);

module_exit(highmem_exit);


 

打印结果:

p = c12386e0

addr = d1c37000

 

获得了高端地址!没有,还是低端地址,这是为什么?因为我的虚拟机内存为512M,不需要高端地址。kmap()里面也有判断:

if (!PageHighMem(page))
    return page_address(page);

return kmap_high(page);

现在我把它调为2G内存。再运行:

p = c1cea000

addr = ff9a2000

在ff8000000地址之上了,酷!

 

这个pkmap是会睡眠的。不能睡眠的话用fixmap。

把上面的kmap()和kunmap()改成kmap_atomic()和kunmap_atomic()就可以了。我改的:

addr = (unsigned long *)kmap_atomic(p,FIX_KMAP_BEGIN);

kunmap_atomic(p, FIX_KMAP_BEGIN);

多了一个参数,下面解释。

运行结果:

p = c1a57ac0

addr = fff77000

 

fixmap概念上和低端的线性地址差不多。不过fixmap可以映射任何物理地址,而且对应的物理地址不必等于线性地址减去0xc0000000,是可以任意方式建立的。它是固定的映射线性地址都映射一个物理内存的页框。

它通过一个枚举fixed_addresses里面的引索来查找线性地址,上面的FIX_KMAP_BEGIN就是其中一个引索。大概地址就是

X86

FIXADDR_TOP - ((idx) << PAGE_SHIFT)

从FIXADDR_TOP向下。

Arm:

(FIXADDR_START + ((idx) <<PAGE_SHIFT))

FIXADDR_START向上,我的板子fixmap  : 0xfff00000 - 0xfffe0000   ( 896 kB)

就是FIXADDR_START向上。

 

当然我看了很多博客,都是说上面的X86,可是没有说明平台的不同。可能处理器默认是x86架构的,操作系统默认是windows的。说到这我就来气,今天领导叫我搞抓去屏幕程序。我第一反应就是linux。他却说是在windows上用MFC、GID、DirectX等去做。嗨~,我这是要被炒鱿鱼的节奏吗?我不求专搞linux内核驱动(也没那个能力),至少让我搞搞linux应用层吧。我一个视linux如命的人叫我搞MFC,额~,我要死了!谁来救救我啊!

当然这些虚拟地址已经在初始化时和物理地址之间建立了映射关系。

 

顺便提一下,上面的分配内存在实际应用时用kmalloc都可以实现,我只是为了弄懂一些事才写这些。

 

现在高端地址还有一位是非连续内存区。

非连续指的就是访问非连续页框。这个优点是避免了外碎片(本来就是碎片组成的),缺点是必须打乱内核页表。

主要的应用:

A. 为活动的交换区分配数据结构。这里会调用vmalloc()来创建与新交换区相关的计数器数组,并把它的地址存放在交换描述符的swap_map字段中。

B. 为模块分配空间,或者给某些IO驱动程序分配缓冲区。就是我们用的ioremap()。

C.提供vmap()。这个都行映射非连续内存区已经分配的页框。和vmalloc的区别就是不分配页框。这个不细说了。

 

看看上面我说是神图的东西,VMALLOC_START于高端内存有8M的安全区,我的arm板打印:

    vmalloc : 0xc4800000 -0xf6000000   ( 792 MB)

    lowmem  : 0xc0000000 - 0xc4000000   (  64MB)

确实有。

每个非连续区之间有4kb的安全区。每个非连续区对应一个vm_struct描述符,它们以链表的形式组织。它包含一个一个flags,它的值如下:

#define VM_IOREMAP      0x00000001      /* ioremap() and friends */

#define VM_ALLOC        0x00000002      /* vmalloc() */

#define VM_MAP          0x00000004      /* vmap()ed pages */

#define VM_USERMAP      0x00000008      /* suitable for remap_vmalloc_range */

#define VM_VPAGES       0x00000010      /* buffer for pages was vmalloc'ed */

我们用vmalloc来分配就会标志为VM_ALLOC,同时这告诉我们ioremap() vmap()也是分配非连续内存。

模拟vmalloc写一段代码,linux-2.6.18下,加上只要一个page,相对于vmalloc()我去掉一些判断,还有vmalloc()对于多于一个页,使用递归调用最终用kmalloc_node()一次只分配一个struct page*。

   area = get_vm_area_node(4096, VM_ALLOC, -1);

   if (!area)

    {

       return -1;

}

 

area->nr_pages= 1;

 

    area->pages = kmalloc(sizeof(structpages*), __GFP_HIGHMEM);

   if (!area->pages)

    {

       remove_vm_area(area->addr);

       kfree(area);

       return -1;

    }

 

   area->flags |= VM_VPAGES;

   memset(area->pages, 0, sizeof(struct pages*));

   area->pages[0] = (struct page *)alloc_page(GFP_KERNEL |__GFP_HIGHMEM);

   if (unlikely(!area->pages[0]))

    {

       area->nr_pages = 0;

       goto fail;

    }

   if (map_vm_area(area, 0x63, &area->pages))

       goto fail;

fail:

    vfree(area->addr);  


 

如果我在简化一下就是:

area = get_vm_area_node(4096, VM_ALLOC,-1);

上面这个是判断线性地址VMALLOC_START和VMALLOC_END之间查找一个空闲区。是否满足要申请的大小和flag。获取线性地址。

area->pages[0] = (struct page*)alloc_page(GFP_KERNEL | __GFP_HIGHMEM);

这个早说过,因为涉及高端内存,只能先获取页描述符地址。下面肯定就是映射了。获取页框。

if (map_vm_area(area, 0x63,&area->pages))

0x63是页框保护位。现在非连续页框也有了,连续的线性地址也有了。现在就是要把它们对应。这就是map_vm_area。

int map_vm_area(struct vm_struct *area,pgprot_t prot, struct page ***pages)

{

       pgd_t *pgd;

       unsigned long next;

       unsigned long addr = (unsigned long) area->addr;

       unsigned long end = addr + area->size - PAGE_SIZE;

       int err;

 

       BUG_ON(addr >= end);

       pgd = pgd_offset_k(addr);获取内核页全局目录中的目录项

       do {

                next = pgd_addr_end(addr, end);

               err = vmap_pud_range(pgd,addr, next, prot, pages);

vmap_pud_range()不细看,就是把物理地址写入内核页全局目录的适合表现。

                if (err)

                        break;

       } while (pgd++, addr = next, addr != end);

       flush_cache_vmap((unsigned long) area->addr, end);

       return err;

}

到这我们知道了之前说的vmalloc的缺点,必须打乱内核页表。当然你不要担心低端内存的页表被打乱,因为不在一个范围。

最后简单说说用户的malloc(),这个才真的叫虚拟,kmalloc和vmalloc是真的让线性地址和页框对应。malloc()应该只是获取线性地址吧!