如图可见,线性地址主要分为两大部分,用户空间和内核空间,其中内核空间分为物理内存映射部分和高端内存映射部分。 本文主要介绍内核空间的高端内存映射部分。 Linux 内核必须处理80X86体系结构的两种硬件约束 ISA总线的直接内存存取(DMA)处理器有一个严格的限制:它们只能对RAM的前16MB寻址。 在具有大容量RAM的现代32位计算机中,CPU不能直接访问所有的物理内存,因为线性地址空间太小。 为了应对这两种限制,Linux 2.6把每个内存节点的物理内存划分为3个管理区(zone) ZONE_DMA 包含低于16MB的内存页框 ZONE_NORMAL 包含高于16MB且低于896MB的内存页框 ZONE_HIGHMEM 包含从896MB开始高于896MB的内存页框
永久内核映射
为了记录高端内存页框与永久内核映射包含的线性地址之间的联系,内核使用了page_address_htable散列表。该表包含一个
page_address_map数据结构,用于高端内存中的每一个页框进行当前映射。而该数据结构还包含一个指向页描述符的指针和分配给该页框的线性地
址。 临时内核映射 在高端内存的任一页都可以通过一个“窗口”(为此而保留的一个页表项)映射到内核地址空间。留给临时内核映射的窗口数是非常少的。 每个CPU都有它自己的包含13个窗口的集合,他们用enum km_type数据结构表示。该数据结构中定义的每个符号,入KM_BOUNCE_READ、KM_USER0或KM_PTE0,标志了窗口的线性地址。 直
接映射的物理内存末尾所对应的线性地址保存在high_memory变量中,从VMALLC_START开始进行高端内存映射,图中看到的8M内存空间是
物理内存映射的末尾与第一个内存区之间的安全区,目的是为了“捕获”对内存的越界访问,出于同样的理由,插入其他4KB大小的安全区来隔离非连续的内存
区。 非连续内存区的描述符 每个非连续区vmallot对应着一个类型为vm_struct的描述符,其数据结构为 类型 名称 说明 Void * Addr 内存区内第一个内存单元的线性地址 Unsigned long Size 内存区的大小加4096(内存区之间的安全区的大小) Unsigned long Flags 非连续内存区映射的内存的类型 Struct page * Pages 指向nr_pages数组的指针,该数组由指向页面描述符组成(物理地址,内存) Unsigned init Nr_pages 内存填充的页的个数 Unsigned long Phys_addr 该字段设为0,除非内存已被创建来映射一个硬件设施的I/O共享内存 Struct vm_struct Next 指向下一个vm_struct结构的指针 通过next字段,这些描述符被插入到一个简单的链表中,链表第一个元素的地址存放在vmlist变量中,对这个链表的访问依靠vmlist_lock读/写自旋锁来保护。 Get_vm_area()函数 在线性地址VMALLOC_START和VMALLOC_END之间查找一个空闲区域。该函数使用两个参数,将被创建的内存区的字节大小(size)和指定空闲区的标志(flag) 1.
调用kmalloc()为vm_struct类型的新描述符获得一个内存区(kmalloc函数返回的是虚拟地址(线性地址).
kmalloc特殊之处在于它分配的内存是物理上连续的,这对于要进行DMA的设备十分重要.
而用vmalloc分配的内存只是线性地址连续,物理地址不一定连续,不能直接用于DMA.) 2. 为写得到vmlist_lock锁,并扫描类型为vm_struct的描述符链表来查找线性地址一个空闲区域,至少覆盖size+4096个地址(4096是内存区之间的安全区间大小) 3. 如果存在这样一个区间,函数就初始化描述符字段,释放vmlist_lock锁,返回这个非连续内存区的起始地址而结束 4. 否则,get_vm_area()释放先前得到的描述符,释放vmlist_lock,然后返回NULL. 分配非连续内存区 Vmalloc()函数给内核分配一个非连续内存区,参数size表示所请求内存区的大小。如果这个函数能够满足请求,就返回新内存区的其实地址,否则,返回一个NULL指针。 void *vmalloc(unsigned long size) { return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL); } 实际上调用__vmalloc: void *__vmalloc(unsigned long size, int gfp_mask, pgprot_t prot) { struct vm_struct *area; struct page **pages; unsigned int nr_pages, array_size, i; //使请求的大小与页框对齐 size = PAGE_ALIGN(size); //有效性检查 if (!size || (size >> PAGE_SHIFT) > num_physpages) return NULL; //取得一个有效的VM,这个函数我们在前面已经详细的分析过了 area = get_vm_area(size, VM_ALLOC); if (!area) return NULL; //所要映射的页面总数 nr_pages = size >> PAGE_SHIFT; //页面描述符所占的空间 array_size = (nr_pages * sizeof(struct page *)); area->nr_pages = nr_pages; area->pages = pages = kmalloc(array_size, (gfp_mask & ~__GFP_HIGHMEM)); //如果空间分配失败 if (!area->pages) { remove_vm_area(area->addr); kfree(area); return NULL; } memset(area->pages, 0, array_size); //为每一个页面分配空间 for (i = 0; i < area->nr_pages; i++) { area->pages[i] = alloc_page(gfp_mask); if (unlikely(!area->pages[i])) { /* Successfully allocated i pages, free them in __vunmap() */ area->nr_pages = i; goto fail; } } //为所分配的页面建立映射关系 (map_vm_area(area, prot, &pages)) goto fail; return area->addr; fail: vfree(area->addr); return NULL; }
map_vm_area为所分配的内存建立映射关系,它的程序流程与unmap_vm_area差不多,都是从pgd找到pte,如果同样的映射关系不
存在,则新建之.(如:pgd对应的pmd不存在,则新建pmd项,使pgd指向建好的pmd.同理,如果pmd所映射的pte项不存在,则新建pte,
然后建立映射),然后将pte映射到相应的页表.代码如下: int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page ***pages) { unsigned long address = (unsigned long) area->addr; unsigned long end = address + (area->size-PAGE_SIZE); pgd_t *dir; int err = 0; //vm 起始地址所在的页目录 dir = pgd_offset_k(address); spin_lock(&init_mm.page_table_lock); do { pmd_t *pmd = pmd_alloc(&init_mm, dir, address); if (!pmd) { err = -ENOMEM; break; } //轮到pmd了 ^_^ if (map_area_pmd(pmd, address, end - address, prot, pages)) { err = -ENOMEM; break; } address = (address + PGDIR_SIZE) & PGDIR_MASK; dir++; } while (address && (address < end)); spin_unlock(&init_mm.page_table_lock); flush_cache_vmap((unsigned long) area->addr, end); return err; } static int map_area_pmd(pmd_t *pmd, unsigned long address, unsigned long size, pgprot_t prot, struct page ***pages) { unsigned long base, end; base = address & PGDIR_MASK; address &= ~PGDIR_MASK; end = address + size; if (end > PGDIR_SIZE) end = PGDIR_SIZE; do { pte_t * pte = pte_alloc_kernel(&init_mm, pmd, base + address); if (!pte) return -ENOMEM; //轮到pte了 ^_^ if (map_area_pte(pte, address, end - address, prot, pages)) return -ENOMEM; address = (address + PMD_SIZE) & PMD_MASK; pmd++; } while (address < end); return 0; } //为页表页建立映射关系 static int map_area_pte(pte_t *pte, unsigned long address, unsigned long size, pgprot_t prot, struct page ***pages) { unsigned long end; address &= ~PMD_MASK; end = address + size; if (end > PMD_SIZE) end = PMD_SIZE; do { struct page *page = **pages; WARN_ON(!pte_none(*pte)); if (!page) return -ENOMEM; //具体的映射在这里了 ^_^ set_pte(pte, mk_pte(page, prot)); address += PAGE_SIZE; pte++; (*pages)++; } while (address < end); return 0; } 除了vmalloc()函数外,非连续内存还能由vmalloc_32()函数分配,该函数与vmalloc()相似,但是它只从ZONE_NORMAL何ZONE_DMA内存管理区中分配框。
|