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.页表和页目录:看图说话
这个图太多人看过了,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 非连续内存区,看下图
如果你只在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()应该只是获取线性地址吧!