kernel hacker修炼之道之内存管理-高端内存(上)

本文深入探讨Linux内核如何管理超过1G的物理内存,通过直接映射和窗口映射策略,实现对高端内存的有效访问。重点解析非连续内存区访问、永久内核映射和临时内核映射三种访问方式,以及内存释放过程中的关键细节。
<style type="text/css"> <!-- @page { margin: 2cm } P { margin-bottom: 0.21cm } PRE.western { font-family: "DejaVu Sans Mono", monospace } PRE.cjk { font-family: "DejaVu Sans", monospace } PRE.ctl { font-family: "DejaVu Sans Mono", monospace } --> </style>

浅析linux内核内存管理之高端内存

作者:李万鹏


进程可以寻址4G,其中0~3G为用户态,3G4G为内核态。如果内存不超过1G那么最后这1G线性空间足够映射物理内存了,如果物理内存大于1G,为了使内核空间的1G线性地址可以访问到大于1G的物理内存,把物理内存分为两部分,0896MB的进行直接内存映射,也就是说存在一个线性关系:virtual address = physical address + PAGE_OFFSET,这里的PAGE_OFFSET3G。还剩下一个128MB的空间,这个空间作为一个窗口动态进行映射,这样就可以访问大于1G的内存,但是同一时刻内核空间还是只有1G的线性地址,只是不同时刻可以映射到不同的地方。综上,大于896MB的物理内存就是高端内存,内核引入高端内存这个概念是为了通过128MB这个窗口访问大于1G的物理内存。

上图是内核空间1G线性地址的布局,直接映射区为PAGE_OFFSETPAGE_OFFSET+ 896MB,直接映射的物理地址末尾对应的线性地址保存在high_memory变量中。直接映射区后边有一个8MB的保护区,目的是用来"捕获"对内存的越界访问。然后是非连续内存区,范围从VMALLOC_START~VMALLOC_END,出于同样的原因,每个非连续内存区之间隔着4KB。永久内核映射区从PKMAP_BASE开始,大小为2MB(启动PAE)4MB。后边是固定映射区,范围是FIXADDR_STARTFIXADDR_TOP,至于临时内核映射区是永久内核映射区里的一部分,在后边会做详细解析。

下边来详细介绍高端内存的三种访问方式:非连续内存区访问,永久内核映射,临时内核映射。


非连续内存区访问:

非连续内存区访问会使用一个vm_struct结构来描述每个非连续内存区:

struct vm_struct { void *addr; //内存区内第一个内存单元的线性地址 unsigned long size; //内存区的大小加4096(内存区之间的安全区间的大小) unsigned long flags; //非连续的内存区映射的内存类型 struct page **pages; //指向nr_pages数组的指针,该数组由指向页描述符的指针组成 unsigned int nr_pages; //内存区填充的页的个数 unsigned long phys_addr; //该字段为0,除非内存已被创建来映射一个硬件设备的I/O共享内存 struct vm_struct *next; //指向下一个vm_struct结构的指针 };

vm_structvmalloc()分配的非连续线性区有如下关系:


下边来看非连续内存区的分配,分配调用了vmalloc()函数:

void *vmalloc(unsigned long size) { return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL); }

flags标志中设置了从high memory分配。

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); /*需要分配的大小不能为0,也不能大于物理页的总数量*/ if (!size || (size >> PAGE_SHIFT) > num_physpages) return NULL; /*找到一个线性区,并获得vm_struct描述符,描述符的flags字段被初始化为VM_ALLOC标志,该标志意味着通过使用vmalloc()函数*/ 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; /* Please note that the recursion is strictly bounded. */ /*如果数组的大小大于页的大小就从非连续内存区分配,否则从kmalloc分配*/ if (array_size > PAGE_SIZE) pages = __vmalloc(array_size, gfp_mask, PAGE_KERNEL); else pages = kmalloc(array_size, (gfp_mask & ~__GFP_HIGHMEM)); area->pages = pages; /*如果这个数组没有分配到内存,就释放vm_struct描述符,返回*/ if (!area->pages) { remove_vm_area(area->addr); kfree(area); return NULL; } /*清空*/ memset(area->pages, 0, array_size); /*从高端内存非配每一个物理页,将描述符的pages字段的每一项指向得到的物理页的page结构*/ 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; } } /*建立页表与物理页之间的映射,一级一级的很复杂*/ if (map_vm_area(area, prot, &pages)) goto fail; return area->addr; fail: vfree(area->addr); return NULL; }调用get_vm_area()分配描述符和获得线性地址空间,get_vm_area()函数在线性地址VMALLOC_START和VMALLOC_END之间查找一个空闲区域。步骤如下:
1. 调用kmalloc()为vm_struct类型的新描述符获得一个内存区。
2. 为写得到vmlist_lock()锁,并扫描类型为vm_struct的描述符链表来查找线性地址一个空闲区域,至少覆盖size + 4096个地址。
3. 如果存在这样一个区间,函数就初始化描述符的字段,释放vmlist_lock锁,并以返回这个非连续内存区的起始地址而结束。
4. 否则,get_vm_area()释放先前得到的描述符,释放vmlist_lock,然后返回NULL。
调用map_vm_area()建立页表与物理页之间的映射:
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); unsigned long next; pgd_t *pgd; int err = 0; int i; /*得到主内核页表中pgd中的相应项的线性地址*/ pgd = pgd_offset_k(address); /*获得主内核页表自旋锁*/ spin_lock(&init_mm.page_table_lock); for (i = pgd_index(address); i <= pgd_index(end-1); i++) { /*调用pud_alloc分配页上级目录*/ pud_t *pud = pud_alloc(&init_mm, pgd, address); if (!pud) { err = -ENOMEM; break; } /*跨越一个pgd所对应内存的大小*/ next = (address + PGDIR_SIZE) & PGDIR_MASK; if (next < address || next > end) next = end; /*建立这个页上级目录所对应的所有页中间目录*/ if (map_area_pud(pud, address, next, prot, pages)) { err = -ENOMEM; break; } address = next; pgd++; } spin_unlock(&init_mm.page_table_lock); flush_cache_vmap((unsigned long) area->addr, end); return err; }

map_area_pud也就是反复调用map_area_pmd来填充各级页目录,页表。map_area_pte()的主循环如下:

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);

调用set_pte设置将相应页的页描述符地址设置到相应的页表项。非连续内存区的释放:

#define mk_pte(page, pgprot) pfn_pte(page_to_pfn(page), (pgprot)) #define pfn_pte(pfn, prot) __pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot)) #define set_pte(pteptr, pteval) (*(pteptr) = pteval)需要注意的是,map_vm_area()并不触及当前进程的页表。
非连续内存区的释放:
调用vfree()函数:
void vfree(void *addr) { BUG_ON(in_interrupt()); __vunmap(addr, 1); }__vunmap()调用remove_vm_area(),执行与map_vm_area()相反的操作,最终调用到了unmap_area_pte():do { pte_t page; page = ptep_get_and_clear(pte); address += PAGE_SIZE; pte++; if (pte_none(page)) continue; if (pte_present(page)) continue; printk(KERN_CRIT "Whee.. Swapped out page in kernel page table\n"); } while (address < end);这里调用ptep_get_and_clear()宏将pte指向的页表项设为0。

注意,在调用vmalloc()时建立的映射是在物理内存和主内核页表之间的,并没有涉及到进程的页表。当内核态的进程访问非连续内存区时,缺页发生,因为该内存区所对应的进程页表中的表项为空。然而,缺页处理程序要检查这个缺页线性地址是否在主内核页表中。一旦处理程序发现一个主内核页表含有这个线性地址的非空项,就把它的值拷贝到相应的进程页表项中,并恢复进程的正常执行。在调用vfree时,与vmalloc()一样,内核修改主内核页全局目录和它的子页表中相应的项,但是映射第4个GB的进程页表的项保持不变。unmap_area_pte()函数只是清除页表中的项(不回收页表本身)。进程对已释放非连续内存区的进一步访问必将由于空的页表项而触发缺页异常。


永久内核映射:

永久内核映射使用主内核页表中一个专门的页表,其地址存放在pkmap_page_table变量中,页表中的表项数由LAST_PKMAP宏产生,因此内核一次访问2MB(启动PAE)或4MB的高端内存。该页表映射的线性地址从PKMAP_BASE开始,pkmap_count数组包含LAST_PKMAPGE个计数器,pkmap_page_table页表中每一项都有一个。

计数器为0

对应的页表项没有映射任何高端内存页框,并且是可用的。

计数器为1

对应的页表项没有映射任何高端内存页框,但是它不能使用,因为此从他最后一次使用以来,其相应的TLB表项还未被刷新。

计数器为2

相应的页表项映射一个高端内存页框,这意味着正好有n-1个内核成分在使用这个页框。

struct page_address_map { struct page *page; void *virtual; struct list_head list; }; static struct page_address_slot { struct list_head lh; /* List of page_address_maps */ spinlock_t lock; /* Protect this bucket's list */ } ____cacheline_aligned_in_smp page_address_htable[1<<PA_HASH_ORDER];

static struct page_address_slot *page_slot(struct page *page) { return &page_address_htable[hash_ptr(page, PA_HASH_ORDER)]; }

为了记录高端内存页框与永久内核映射的线性地址之间的联系,内核使用了page_address_htable散列表,该表包含一个page_address_htable结构,该表包含一个page_address_map数据结构,用于为高端内存每一个页框进行当前映射。而该数据结构还包含一个指向页描述符的指针和分配给该页框的线性地址。page是一个指向全局mem_map数组中的page实例的指针,virtual指定了该页在内核虚拟地址空间中分配的位置。为了便于组织,映射保存在散列表中,结构中的链表元素用于建立溢出的链表,以处理散列碰撞。散列表为page_address_htable,散列函数是page_slot,根据page实例确定页的虚拟地址。如果page是在普通内存中的,则根据page在mem_map数组中的位置计算。

进行永久内核映射需要调用kmap()函数:

void *kmap(struct page *page) { might_sleep(); if (!PageHighMem(page)) return page_address(page); return kmap_high(page); } kmap函数只是一个前端,用于确定指定的页是否确实在高端内存域中。首先判断是不是高端内存,如果不是高端内存就调用page_address直接返回page对应的线性地址,0~896MB的是直接映射,也就是在kernel初始化的时候映射已经建立好了,之后直接访问就行了。而高端内存需要自己建立映射,然后才能访问。如果是高端内存则将工作委托给kmap_high:

void fastcall *kmap_high(struct page *page) { unsigned long vaddr; /* * For highmem pages, we can't trust "virtual" until * after we have the lock. * * We cannot call this from interrupts, as it may block */ spin_lock(&kmap_lock); vaddr = (unsigned long)page_address(page); if (!vaddr) vaddr = map_new_virtual(page); pkmap_count[PKMAP_NR(vaddr)]++; if (pkmap_count[PKMAP_NR(vaddr)] < 2) BUG(); spin_unlock(&kmap_lock); return (void*) vaddr; } 首先获得需要映射的page对应的线性地址,从page_address_htable中进行查找,如果已经映射过了肯定不为空,如果没有映射过则执行map_new_virtual进行映射。注意这里进行的pkmap_count加一操作,后边会提到。

static inline unsigned long map_new_virtual(struct page *page) { unsigned long vaddr; int count; start: count = LAST_PKMAP; /* Find an empty entry */ for (;;) { last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK; if (!last_pkmap_nr) { flush_all_zero_pkmaps(); count = LAST_PKMAP; } 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); __set_current_state(TASK_UNINTERRUPTIBLE); add_wait_queue(&pkmap_map_wait, &wait); spin_unlock(&kmap_lock); schedule(); remove_wait_queue(&pkmap_map_wait, &wait); spin_lock(&kmap_lock); /* 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(&(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot)); kmap_count[last_pkmap_nr] = 1; set_page_address(page, (void *)vaddr); return vaddr; } 内核将上次使用过的页表项的索引保存在last_pkmap_nr变量中,避免了重复查找。如果找到计数器为0的,则获得这个页表项对应的页表的线性地址,填充相应的页表项,计数器为1。这里产生一个问题,刚才不是说为1代表“对应的页表项没有映射任何高端内存页框,但是它不能使用,因为此从他最后一次使用以来,其相应的TLB表项还未被刷新。”,确实是这样,看看调用map_new_virtual函数的kmap_high函数,在这里对pkmap_count进行了再次加一。
void kunmap(struct page *page) { if (in_interrupt()) BUG(); if (!PageHighMem(page)) return; kunmap_high(page); } kmap会导致进程阻塞,所以永久内核映射不能运行在中断处理程序中和可延迟函数的内部。所以这里kunmap首先判断是不是在中断上下文中,如果不在中断上下文并且不再高端内存,则将工作委托kunmap_high。如果有计数器为1的表项,而且有等待的进程,则唤醒进程。如果有进程被阻塞那么肯定没有计数器为0的表项了,所以从map_new_virtual中start标号的地方开始执行,应该会把表项遍历一圈,此时肯定会遇到last_pkmap_nr为0的情况,此时就可以调用flush_all_zero_pkmaps()函数来寻找计数器为1的表项,将其清零,解除映射,并刷新TLB。可以看到kunmap并没有解除映射,并刷新tlb,这里仅仅是将表项的计数器减一。为什么要这样做呢?看一下kmap_high函数中,判断了一次是否已经映射了,如果映射了就把表项的引用计数器加一,也就是说如果调用过kunmap使计数为1了,此时又变成2,又可以进行访问了,不用重新填充页表,刷新tlb等。
void fastcall kunmap_high(struct page *page) { unsigned long vaddr; unsigned long nr; int need_wakeup; spin_lock(&kmap_lock); vaddr = (unsigned long)page_address(page); if (!vaddr) BUG(); nr = PKMAP_NR(vaddr); /* * A count must never go down to zero * without a TLB flush! */ need_wakeup = 0; switch (--pkmap_count[nr]) { case 0: BUG(); case 1: need_wakeup = waitqueue_active(&pkmap_map_wait); } spin_unlock(&kmap_lock); /* do wake-up, if needed, race-free outside of the spin lock */ if (need_wakeup) wake_up(&pkmap_map_wait); }


Linux常见驱动源码分析(kernel hacker修炼)--李万鹏 李万鹏 IBM Linux Technology Center kernel team 驱动资料清单内容如下: Linux设备模型(中)之上层容器.pdf Linux设备模型(上)之底层模型.pdf Linux驱动修炼-驱动中一些常见的宏.pdf Linux驱动修炼-内存映射.pdf Linux驱动修炼-看门狗框架源码分析.pdf Linux驱动修炼-触摸屏驱动之s3c2410_ts源码分析.pdf Linux驱动修炼-SPI驱动框架源码分析().pdf Linux驱动修炼-SPI驱动框架源码分析().pdf Linux驱动修炼-SPI驱动框架源码分析().pdf Linux驱动修炼-RTC子系统框架源码分析.pdf Linux驱动修炼-platform.pdf Linux驱动修炼-LCD背光gpio控制.pdf Linux驱动修炼-INPUT子系统().pdf Linux驱动修炼-INPUT子系统().pdf Linux驱动修炼-framebuffer(中).pdf Linux驱动修炼-framebuffer(下).pdf Linux驱动修炼-framebuffer(上).pdf Linux驱动修炼-DMA框架源码分析().pdf Linux驱动修炼-DMA框架源码分析().pdf Linux驱动修炼-DM9000A网卡驱动框架源码分析().pdf Linux驱动修炼-DM9000A网卡驱动框架源码分析().pdf Linux驱动修炼-DM9000A网卡驱动框架源码分析().pdf Linux驱动修炼-clock框架.pdf Linux驱动修炼-ADC驱动.pdf Linux内核访问外设I O资源的方式.pdf LINUX内核USB子系统学习笔记之初识USB.pdf kernel hacker修炼之驱动-流水灯.pdf kernel hacker修炼之驱动-混杂设备.pdf kernel hacker修炼之驱动-按键.pdf kernel hacker修炼之PCI subsystem().pdf kernel hacker修炼之PCI subsystem().pdf kernel hacker修炼之PCI subsystem().pdf kernel hacker修炼之PCI subsystem().pdf kernel hacker修炼之PCI subsystem().pdf
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值