摘要:本文主要讲述linux如何处理ARM cortex A9多核处理器的内存管理部分。包括对kmap、kunmap函数的分析。
在32位操作系统中,内核虚拟地址空间一般只有1G,可能远小于整个物理地址空间的大小。内核没有进行线性映射的的高端内存,内核并不能直接访问它们。也就是说;分配页框线性地址的页分配器函数不适用于高端内存。
如果内核调用__get_free_page(GFP_HIGHMEM,0)在高端内存分配一个页框,那么__get_free_pages不能返回它的线性地址,因为线性地址根本不存在。因此,函数返回NULL,它甚至不能释放该页框。也就是说,向__get_free_page传递GFP_HIGHMEM是非法的。如果您这样做的话,内核会警告您。
在64位硬件平台上没有这个问题。因为我们的物理地址应该远小于2^64字节,内核虚拟地址空间完全可以对整个物理地址空间进行线性映射。在32位硬件平台上,要支持64GB的物理内存,必须找到一种方法。目前的方法是:
1、高端内存只能通过alloc_pages和alloc_page分配。这两个函数返回每一个被分配页框的页描述符的线性地址。而不会返回线性地址。因为线性地址根本不存在。而它的描述符地址总是存在的。
2、高端内存中的页框不能被内核访问,除非它们拥有线性地址。为此,内存专门留下一部分虚拟地址空间用于映射高端内存页框。当然,这种映射是暂时的。
内核可以采用三种不同的机制将高端内存中的页框映射到内核虚拟地址中:永久内核映射、临时内核映射、非连续内存分配。我们这里要讨论的是永久内核映射、临时内核映射。相应的内核处理函数是kmap和kmap_atomic。
永久内核映射可能阻塞当前进程。这发生在保留的虚拟地址空间紧张时。所以,永久内核映射不能用于中断处理程序和软中断函数。相反,临时内核映射不会阻塞当前进程,不过,它的缺点是只有极少(大约13个页面)临时内核映射可以同时建立起来。
- /* 永久内核映射 */
- void *kmap(struct page *page)
- {
- /* 调试代码,当打开相关调试选项,并且在不可阻塞上下文(如中断代码中)调用此函数时,系统会警告 */
- might_sleep();
- /* 如果页面不是高端内存,则直接返回其内核线性地址。因为此时不需要进行额外的映射 */
- if (!PageHighMem(page))
- return page_address(page);
- /* 如果是高端内存,则分配一个内核虚拟地址,并建立页表项映射到该物理地址上 */
- return kmap_high(page);
- }
page_address函数获得一个页面的内核虚拟地址
- /* 获得一个页面的内核虚拟地址 */
- void *page_address(struct page *page)
- {
- unsigned long flags;
- void *ret;
- struct page_address_slot *pas;
- /**
- * 如果不是高端内存,那么直接返回它的线性地址,因为内核可以直接访问这样的内存。
- * 当然,在arm\x86架构中,还是需要pte页表项才能访问线性地址。
- * 对powerpc和mips来说,访问线性地址的机制不太一样
- */
- if (!PageHighMem(page))
- /* 返回页面物理地址加上固定的偏移(典型情况下,偏移值是3G),即返回其线性地址 */
- return lowmem_page_address(page);
- /**
- * 否则页框在高端内存中(PG_highmem标志为1),则到page_address_htable散列表中查找。
- * 该哈希表记录了非线性性映射的所有页面。
- * 这里是查找页面所在的哈希桶
- */
- pas = page_slot(page);
- ret = NULL;
- /* 关中断并获得哈希桶链表的自旋锁 */
- spin_lock_irqsave(&pas->lock, flags);
- if (!list_empty(&pas->lh)) {/* 映射高端内存的情况毕竟是少数,哈希桶一般是空的,这里判断哈希桶是否为空 */
- struct page_address_map *pam;
- /* 如果哈希桶不为空,则遍历其中的第一项 */
- list_for_each_entry(pam, &pas->lh, list) {
- if (pam->page == page) {/* 如果当前页面在桶中 */
- ret = pam->virtual;/* 取得该页面的内核虚拟地址,并返回 */
- goto done;
- }
- }
- }
- /**
- * 没有在page_address_htable中找到,返回默认值NULL。
- */
- done:
- /* 释放自旋锁并开中断 */
- spin_unlock_irqrestore(&pas->lock, flags);
- return ret;
- }
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.
- */
- /**
- * 获得kmap_lock自旋锁,这把锁有两个作用:
- * 1:确保只对page进行一次kmap映射,如果两个地方同时调用kmap映射页面,则只有一次kmap进行真正的映射。
- * 2:kmap需要分配一个可用内核虚拟地址,这里保护虚拟地址空间分配时用到的数据结构。
- */
- lock_kmap();
- /* 再次获取页面的虚拟地址 */
- vaddr = (unsigned long)page_address(page);
- if (!vaddr)/* 如果确实没有对该页面进行映射,则调用map_new_virtual获得一个可用的虚拟地址 */
- /* 这里可能导致阻塞,因为虚拟地址空间可能不足,需要等待 */
- vaddr = map_new_virtual(page);
- /* 将该虚拟地址对应的映射计数加1,如果多次映射,则直到最后一次kunmap调用完毕后才真正解除pte页表项 */
- pkmap_count[PKMAP_NR(vaddr)]++;
- /**
- * 初次映射时,map_new_virtual中会将计数置为1,上一句再加1.
- * 多次映射时,计数值会再加1.
- * 总之,计数值决不会小于2.
- */
- BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
- /* 释放kmap_lock自旋锁 */
- unlock_kmap();
- return (void*) vaddr;
- }
为某个页面第一次建立kmap映射时,会调用map_new_virtual函数:
- /**
- * 为建立永久内核映射建立初始映射.
- */
- static inline unsigned long map_new_virtual(struct page *page)
- {
- unsigned long vaddr;
- int count;
- start:
- /* 允许搜索的次数,由于我们在下面的循环中可能进行两次遍历,两次遍历的总比较次数应该是kmap虚拟地址空间总页面数量(512) */
- count = LAST_PKMAP;
- /* Find an empty entry */
- /**
- * 扫描pkmap_count中的所有计数器值,直到找到一个空值.空值表示该虚拟地址还没有被kmap占用
- */
- for (;;) {
- /**
- * 从上次结束的地方开始搜索.
- */
- last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
- /**
- * 搜索到最后一位了.再从0开始搜索前,刷新计数为1的项.
- * 当计数值为1表示页表项可用,但是对应的TLB还没有刷新(没有失效,即其pte页表项还存在).
- */
- if (!last_pkmap_nr) {
- flush_all_zero_pkmaps();
- count = LAST_PKMAP;
- }
- /**
- * 找到计数为0的页表项,表示该页空闲且可用.
- */
- if (!pkmap_count[last_pkmap_nr])
- break; /* Found a usable entry */
- /**
- * count是允许的搜索次数.如果还允许继续搜索下一个页表项.则继续,否则表示搜索完所有kmap地址项,没有空闲项,退出.
- */
- if (--count)
- continue;
- /*
- * Sleep for somebody else to unmap their entries
- */
- /**
- * 运行到这里,表示没有找到空闲页表项.先睡眠一下.
- * 等待其他线程释放页表项,然后唤醒本线程.
- */
- {
- DECLARE_WAITQUEUE(wait, current);
- __set_current_state(TASK_UNINTERRUPTIBLE);
- /**
- * 将当前线程挂到pkmap_map_wait等待队列上.
- */
- add_wait_queue(&pkmap_map_wait, &wait);
- /* 开始睡眠前,必须释放自旋锁 */
- unlock_kmap();
- schedule();
- /* 再次调度回来,说明其他线程释放了kmap虚拟地址空间,唤醒了本线程,首先将自己从等待队列中摘除 */
- remove_wait_queue(&pkmap_map_wait, &wait);
- /* 重新获得kmap_lock自旋锁 */
- lock_kmap();
- /* Somebody else might have mapped it while we slept */
- /**
- * 在当前线程等待的过程中,其他线程可能已经将页面进行了映射.
- * 检测一下,如果已经映射了,就退出.
- * 注意,这里没有对kmap_lock进行解锁操作.关于kmap_lock锁的操作,需要结合kmap_high来分析.
- * 总的原则是:进入本函数时保证关锁,然后在本句前面关锁,本句后面解锁.
- * 在函数返回后,锁仍然是关的.则外层解锁.
- * 即使在本函数中循环也是这样.
- * 内核就是这么让人感到迷糊,看久了就习惯了.不过你目前可能必须得学着适应这种代码.
- */
- if (page_address(page))
- return (unsigned long)page_address(page);
- /* Re-start */
- goto start;
- }
- }
- /**
- * 不管何种路径运行到这里来,kmap_lock都是锁着的.
- * 并且last_pkmap_nr对应的是一个空闲且可用的表项.
- */
- vaddr = PKMAP_ADDR(last_pkmap_nr);
- /**
- * 设置页表属性,建立虚拟地址和物理地址之间的映射.
- */
- set_pte_at(&init_mm, vaddr,
- &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
- /**
- * 1表示相应的项可用,但是TLB需要刷新.注意前面的set_pte_at仅仅是建立了页面项,硬件tlb还等待刷新。
- */
- pkmap_count[last_pkmap_nr] = 1;
- /* 在page_address_htable哈希表中建立页面和虚拟地址之间的映射关系,在page_address函数中会用到 */
- set_page_address(page, (void *)vaddr);
- return vaddr;
- }
到此,kmap的流程已经注释完毕,我们看一下释放映射的函数kunmap:
- /**
- * 解除高端内存的永久内核映射
- */
- void kunmap(struct page *page)
- {
- /* 不可能在中断中调用kmap,因此也不可能在中断中解除映射。这里用BUG_ON来确保检测是否有这种错误情况出现 */
- BUG_ON(in_interrupt());
- /**
- * 如果对应页根本就不是高端内存,当然就没有进行内核映射,也就不用调用本函数了。
- */
- if (!PageHighMem(page))
- return;
- /**
- * kunmap_high真正执行unmap过程
- */
- kunmap_high(page);
- }
实际的解决映射处理是在kunmap_high函数中:
- /**
- * 解除高端内存的永久内核映射
- */
- void kunmap_high(struct page *page)
- {
- unsigned long vaddr;
- unsigned long nr;
- unsigned long flags;
- int need_wakeup;
- /* 获得kmap_lock自旋锁 */
- lock_kmap_any(flags);
- /**
- * 得到物理页对应的虚拟地址。
- */
- vaddr = (unsigned long)page_address(page);
- /**
- * vaddr==0,可能是内存越界等严重故障了吧。或者是误调用
- * BUG一下
- */
- BUG_ON(!vaddr);
- /**
- * 根据虚拟地址,找到页表项在pkmap_count中的序号。
- */
- nr = PKMAP_NR(vaddr);
- /*
- * A count must never go down to zero
- * without a TLB
- */
- need_wakeup = 0;
- switch (--pkmap_count[nr]) {/* 首先将该页面的映射计数减1 */
- case 0:/* 一定是逻辑错误了,多次调用了unmap */
- BUG();
- case 1:/* 1表示该虚拟地址已经没有任何人映射了,可被其他线程使用 */
- /*
- * Avoid an unnecessary wake_up() function call.
- * The common case is pkmap_count[] == 1, but
- * no waiters.
- * The tasks queued in the wait-queue are guarded
- * by both the lock in the wait-queue-head and by
- * the kmap_lock. As the kmap_lock is held here,
- * no need for the wait-queue-head's lock. Simply
- * test if the queue is empty.
- */
- /**
- * 页表项可用了。need_wakeup会判断是否有等待唤醒。
- * 如果有线程在等待kmap虚拟地址空间的话。
- */
- need_wakeup = waitqueue_active(&pkmap_map_wait);
- }
- /* 释放自旋锁 */
- unlock_kmap_any(flags);
- /* do wake-up, if needed, race-free outside of the spin lock */
- if (need_wakeup)/* 如果有线程在等待可用地址空间,则唤醒它 */
- wake_up(&pkmap_map_wait);
- }
如果想在中断上下文中访问高端内存,不能使用kmap,而需要调用kmap_atomic。我们称这个函数为临时内核映射。
其定义为:
- #define kmap_atomic(page, args...) __kmap_atomic(page)
感觉这个宏怪怪的,其实是为了代码兼容的需要。因为旧版本内核是这样定义的:
- void *kmap_atomic(struct page *page, enum km_type type)
相应的,旧版本这样调用kmap_atomic(page, KM_XXXX),新版本去掉了type。这样,即使驱动中有代码传入第二个参数,在新版本中将被忽略掉。
为什么新版本不象旧版本那样定义一个km_type枚举类型,并在不同的上下文指定不同的调用参数呢?这个问题留给聪明的读者作为练习。
我们看看__kmap_atomic的实现:
- /**
- * 建立临时内核映射
- */
- void *__kmap_atomic(struct page *page)
- {
- unsigned int idx;
- unsigned long vaddr;
- void *kmap;
- int type;
- /**
- * 这里其实是禁止抢占
- * 禁止抢占的目的是为了避免线程飘移到其他核,因为后面要使用smp_processor_id确定线程的所在CPU
- * 不同CPU占用的kmap虚拟地址空间不一样,读者可以认真思考一下为什么需要这样做。
- */
- pagefault_disable();
- if (!PageHighMem(page))/* 如果页面不是高端内存 */
- return page_address(page);/* 直接返回其线性地址即可,没有必要进行kmap映射 */
- #ifdef CONFIG_DEBUG_HIGHMEM
- /*
- * There is no cache coherency issue when non VIVT, so force the
- * dedicated kmap usage for better debugging purposes in that case.
- */
- if (!cache_is_vivt())
- kmap = NULL;
- else
- #endif
- /* 在获得锁的情况下,获取页面的kmap地址,这里主要是防止不同任务、不同CPU上对同一个页面进行多次映射 */
- kmap = kmap_high_get(page);
- if (kmap)/* 如果其他地方已经映射了该页,则在kmap_high_get中已经增加了映射计数,这里直接返回其虚拟地址即可 */
- return kmap;
- /* 递增本CPU上可用的kmap虚拟地址索引号 */
- type = kmap_atomic_idx_push();
- /* 每个CPU上的kmap虚拟地址空间不同,这里是计算本CPU可用的地址索引号 */
- idx = type + KM_TYPE_NR * smp_processor_id();
- /* 将kmap地址索引号转换为可用的虚拟地址 */
- vaddr = __fix_to_virt(FIX_KMAP_BEGIN + idx);
- #ifdef CONFIG_DEBUG_HIGHMEM
- /*
- * With debugging enabled, kunmap_atomic forces that entry to 0.
- * Make sure it was indeed properly unmapped.
- */
- BUG_ON(!pte_none(*(TOP_PTE(vaddr))));
- #endif
- /* 为虚拟地址建立pte页表项 */
- set_pte_ext(TOP_PTE(vaddr), mk_pte(page, kmap_prot), 0);
- /*
- * When debugging is off, kunmap_atomic leaves the previous mapping
- * in place, so this TLB flush ensures the TLB is updated with the
- * new mapping.
- */
- /* 清空tlb中的无效索引,因为以前可能建立该虚拟地址的pte页表项并且在tlb存在无效tlb项,要让新pte页表项生效,必须使旧的tlb项失效 */
- local_flush_tlb_kernel_page(vaddr);
- return (void *)vaddr;
- }
要撤销由kmap_atomic映射的高端内存,可以使用kunmap_atomic函数。
- /**
- * 撤销内核临时映射
- */
- void __kunmap_atomic(void *kvaddr)
- {
- /* 将虚拟地址对齐到页边界 */
- unsigned long vaddr = (unsigned long) kvaddr & PAGE_MASK;
- int idx, type;
- /* FIXADDR_START是kmap_atomic映射的最小的虚拟地址,这里验证一下虚拟地址,确保它确实是由kmap_atomic而不是kmap映射出来的 */
- if (kvaddr >= (void *)FIXADDR_START) {
- /* 获得上一次调用kmap_atomic占用的虚拟地址空间索引号 */
- type = kmap_atomic_idx();
- /* 计算该索引号在整个kmap虚拟地址空间中的索引 */
- idx = type + KM_TYPE_NR * smp_processor_id();
- if (cache_is_vivt())/* ??? */
- __cpuc_flush_dcache_area((void *)vaddr, PAGE_SIZE);
- #ifdef CONFIG_DEBUG_HIGHMEM
- BUG_ON(vaddr != __fix_to_virt(FIX_KMAP_BEGIN + idx));
- set_pte_ext(TOP_PTE(vaddr), __pte(0), 0);
- local_flush_tlb_kernel_page(vaddr);
- #else
- (void) idx; /* to kill a warning */
- #endif
- /* 其实这里什么也没有做,仅仅是递减了本CPU上的虚拟地址空间索引号,也就是说下次调用kmap_atomic时,占用本次释放的虚拟地址 */
- kmap_atomic_idx_pop();
- /* 判断虚拟地址空间是否是kmap占用的空间 */
- } else if (vaddr >= PKMAP_ADDR(0) && vaddr < PKMAP_ADDR(LAST_PKMAP)) {
- /* this address was obtained through kmap_high_get() */
- /* 如果是kmap的地址空间,则调用者应当调用kunmap,这里实现一个容错处理。 */
- kunmap_high(pte_page(pkmap_page_table[PKMAP_NR(vaddr)]));
- }
- /**
- * 打开抢占,在kmap_atomic函数中关闭了抢占,这里打开。
- * 换句话说,在kmap_atomic和kunmap_atomic之间,都是关抢占的。大家需要认真思考一下这里不关闭抢占的严重后果。
- * 如果您想明白了,请联系我:scxby@163.com
- */
- pagefault_enable();
- }
如果您足够细心的话,将会发现,除了递减当前CPU上的地址索引号外,kunmap_atomic几乎没有做任何事情。如果您再细心的分析旧版本的内核,会发现什么有效的事情都没有做。甚至没有将tlb失效。
这会造成什么结果呢?如果你调用kunmap_atomic后,继续使用这个虚拟地址,将会发现没有任何问题。没有oops,也没有段错误。
对临时内核映射和永久内核映射的分析就到这里,在下一篇文章中我们继续分析vmalloc和vfree。明天见。
阅读(6925) | 评论(10) | 转发(6) |