精华内容
下载资源
问答
  • 高端内存映射
    千次阅读
    2018-12-23 16:32:23

    我们知道系统中的高端内存是不能作为直接映射区映射到内核空间的,那么我们想要使用它怎么办呢?前面的文章我们已经有过相关的介绍,可以使用三种方法,分别是pkmap(永久映射)/fixmap(临时固定映射)/vmlloc,本文主要介绍fixmap,也就是固定映射又叫临时映射。

    关键点

    通过前面的介绍我们知道,fixmap和pkmap的最大区别是fixmap不会被阻塞,因此可以在中断上下文中使用。那会不会遇到一个问题呢?pkmap在申请映射时发现映射区已经被申请完了所以才会阻塞等待,那么fixmap如何处理这种场景呢?实际上每次fixmap都是可以成功执行映射的,原因是fixmap不会动态申请映射,它是以固定映射的方式进行的,如果我们传入了一个映射type,它就肯定会执行对应的映射,而把之前的映射冲刷掉,因此fixmap的映射关系是靠内核代码来进行维护的,所以内核需要保证同一个映射区不被重复使用,从而出现错误。

    入口函数

    kmap_atomic:

      void *kmap_atomic(struct page *page)
     {
         unsigned int idx;
         unsigned long vaddr;
         void *kmap;
         int type;
     
         pagefault_disable();
         if (!PageHighMem(page))
             return page_address(page);
     
     #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 = kmap_high_get(page);
         if (kmap)
             return kmap;
     
         type = kmap_atomic_idx_push();
     
         idx = type + KM_TYPE_NR * smp_processor_id();
         vaddr = __fix_to_virt(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(get_fixmap_pte(vaddr)));
    #endif
        /*
         * When debugging is off, kunmap_atomic leaves the previous mapping
         * in place, so the contained TLB flush ensures the TLB is updated
         * with the new mapping.
         */
        set_fixmap_pte(idx, mk_pte(page, kmap_prot));
    
        return (void *)vaddr;
    }
    
    

    内核中为每个CPU定义了KM_TYPE_NR个fixmap页面,每个页面地址用一个type来表示,那么我们寻找一个对应type的地址时可以使用如下方式:

    
    #define KM_TYPE_NR 16
    
     idx = type + KM_TYPE_NR * smp_processor_id();
     vaddr = __fix_to_virt(idx);
    
    

    内核中对type在linux-4.0版本中没有明确的定义,只是通过如下方式进行了获取,实际上时按照调用顺序来进行分配的。

     DECLARE_PER_CPU(int, __kmap_atomic_idx);
     
     static inline int kmap_atomic_idx_push(void)
     {
         int idx = __this_cpu_inc_return(__kmap_atomic_idx) - 1;
     
     #ifdef CONFIG_DEBUG_HIGHMEM
         WARN_ON_ONCE(in_irq() && !irqs_disabled());
         BUG_ON(idx >= KM_TYPE_NR);
     #endif
         return idx;
     }   
    
    
    更多相关内容
  • 日期 内核版本 架构 作者 GitHub CSDN 2016-09-29 ... 在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. Linux内核

    原文链接:https://blog.csdn.net/gatieme/article/details/52705142

    日期内核版本架构作者GitHubCSDN
    2016-09-29Linux-4.7X86 & armgatiemeLinuxDeviceDriversLinux内存管理

    在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法.

    Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述.

    伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者.

    • 内核如何记住哪些内存块是空闲的

    • 分配空闲页面的方法

    • 影响分配器行为的众多标识位

    • 内存碎片的问题和分配器如何处理碎片

    1 高端内存与内核映射


    尽管vmalloc函数族可用于从高端内存域向内核映射页帧(这些在内核空间中通常是无法直接看到的), 但这并不是这些函数的实际用途.

    重要的是强调以下事实 : 内核提供了其他函数用于将ZONE_HIGHMEM页帧显式映射到内核空间, 这些函数与vmalloc机制无关. 因此, 这就造成了混乱.

    而在高端内存的页不能永久地映射到内核地址空间. 因此, 通过alloc_pages()函数以__GFP_HIGHMEM标志获得的内存页就不可能有逻辑地址.

    在x86_32体系结构总, 高于896MB的所有物理内存的范围大都是高端内存, 它并不会永久地或自动映射到内核地址空间, 尽管X86处理器能够寻址物理RAM的范围达到4GB(启用PAE可以寻址64GB), 一旦这些页被分配, 就必须映射到内核的逻辑地址空间上. 在x86_32上, 高端地址的页被映射到内核地址空间(即虚拟地址空间的3GB~4GB)

    内核地址空间的最后128 MiB用于何种用途呢?

    该部分有3个用途。

    1. 虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配. 该机制通常用于用户过程, 内核自身会试图尽力避免非连续的物理地址。内核通常会成功,因为大部分大的内存块都在启动时分配给内核,那时内存的碎片尚不严重。但在已经运行了很长时间的系统上, 在内核需要物理内存时, 就可能出现可用空间不连续的情况. 此类情况, 主要出现在动态加载模块时.

    2. 持久映射用于将高端内存域中的非持久页映射到内核中

    3. 固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以自由选择. 它与通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间的关联可以自行定义,关联建立后内核总是会注意到的.

    x86_32上的地址划分

    在这里有两个预处理器符号很重要 __VMALLOC_RESERVE设置了vmalloc区域的长度, 而MAXMEM则表示内核可以直接寻址的物理内存的最大可能数量.

    内核虚拟地址空间

    内核中, 将内存划分为各个区域是通过图3-15所示的各个常数控制的。根据内核和系统配置, 这些常数可能有不同的值。直接映射的边界由high_memory指定。

    1. 直接映射区
      线性空间中从3G开始最大896M的区间, 为直接内存映射区,该区域的线性地址和物理地址存在线性转换关系:线性地址=3G+物理地址。

    2. 动态内存映射区
      该区域由内核函数vmalloc来分配, 特点是 : 线性空间连续, 但是对应的物理空间不一定连续. vmalloc分配的线性地址所对应的物理页可能处于低端内存, 也可能处于高端内存.

    3. 永久内存映射区
      该区域可访问高端内存. 访问方法是使用alloc_page(_GFP_HIGHMEM)分配高端内存页或者使用kmap函数将分配到的高端内存映射到该区域.

    4. 固定映射区
      该区域和4G的顶端只有4k的隔离带,其每个地址项都服务于特定的用途,如ACPI_BASE等。

    说明

    注意用户空间当然可以使用高端内存,而且是正常的使用,内核在分配那些不经常使用的内存时,都用高端内存空间(如果有),所谓不经常使用是相对来说的,比如内核的一些数据结构就属于经常使用的,而用户的一些数据就属于不经常使用的。用户在启动一个应用程序时,是需要内存的,而每个应用程序都有3G的线性地址,给这些地址映射页表时就可以直接使用高端内存。

    而且还要纠正一点的是:那128M线性地址不仅仅是用在这些地方的,如果你要加载一个设备,而这个设备需要映射其内存到内核中,它也需要使用这段线性地址空间来完成,否则内核就不能访问设备上的内存空间了.

    总之,内核的高端线性地址是为了访问内核固定映射以外的内存资源。进程在使用内存时,触发缺页异常,具体将哪些物理页映射给用户进程是内核考虑的事情. 在用户空间中没有高端内存这个概念.

    即内核对于低端内存, 不需要特殊的映射机制, 使用直接映射即可以访问普通内存区域, 而对于高端内存区域, 内核可以采用三种不同的机制将页框映射到高端内存 : 分别叫做永久内核映射临时内核映射以及非连续内存分配

    2 持久内核映射


    如果需要将高端页帧长期映射(作为持久映射)到内核地址空间中, 必须使用kmap函数. 需要映射的页用指向page的指针指定,作为该函数的参数。该函数在有必要时创建一个映射(即,如果该页确实是高端页), 并返回数据的地址.

    如果没有启用高端支持, 该函数的任务就比较简单. 在这种情况下, 所有页都可以直接访问, 因此只需要返回页的地址, 无需显式创建一个映射.

    如果确实存在高端页, 情况会比较复杂. 类似于vmalloc, 内核首先必须建立高端页和所映射到的地址之间的关联. 还必须在虚拟地址空间中分配一个区域以映射页帧, 最后, 内核必须记录该虚拟区域的哪些部分在使用中, 哪些仍然是空闲的.

    2.1 数据结构


    内核在IA-32平台上在vmalloc区域之后分配了一个区域, 从PKMAP_BASEFIXADDR_START. 该区域用于持久映射. 不同体系结构使用的方案是类似的.

    永久内核映射允许内核建立高端页框到内核地址空间的长期映射。 他们使用着内核页表中一个专门的页表, 其地址存放在变量pkmap_page_table中, 页表中的表项数由LAST_PKMAP宏产生. 因此,内核一次最多访问2MB或4MB的高端内存.

    #define PKMAP_BASE              (PAGE_OFFSET - PMD_SIZE)

    页表映射的线性地址从PKMAP_BASE开始. pkmap_count数组包含LAST_PKMAP个计数器,pkmap_page_table页表中的每一项都有一个。

    //  http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L126
    static int pkmap_count[LAST_PKMAP];
    static  __cacheline_aligned_in_smp DEFINE_SPINLOCK(kmap_lock);
    
    pte_t * pkmap_page_table;

    高端映射区逻辑页面的分配结构用分配表(pkmap_count)来描述,它有1024项,对应于映射区内不同的逻辑页面。当分配项的值等于0时为自由项,等于1时为缓冲项,大于1时为映射项。映射页面的分配基于分配表的扫描,当所有的自由项都用完时,系统将清除所有的缓冲项,如果连缓冲项都用完时,系统将进入等待状态。

    // http://lxr.free-electrons.com/source/mm/highmem.c?v=4.7#L126
    /* 
    高端映射区逻辑页面的分配结构用分配表(pkmap_count)来描述,它有1024项, 
    对应于映射区内不同的逻辑页面。当分配项的值等于零时为自由项,等于1时为 
    缓冲项,大于1时为映射项。映射页面的分配基于分配表的扫描,当所有的自由 
    项都用完时,系统将清除所有的缓冲项,如果连缓冲项都用完时,系 
    统将进入等待状态。 
    */  
    static int pkmap_count[LAST_PKMAP];

    pkmap_count(在mm/highmem.c?v=4.7, line 126定义)是一容量为LAST_PKMAP的整数数组, 其中每个元素都对应于一个持久映射页。它实际上是被映射页的一个使用计数器,语义不太常见.

    内核可以通过get_next_pkmap_nr获取到pkmap_count数组中元素的个数, 该函数定义在mm/highmem.c?v=4.7, line 66

    /*
     * Get next index for mapping inside PKMAP region for page with given color.
     */
    static inline unsigned int get_next_pkmap_nr(unsigned int color)
    {
        static unsigned int last_pkmap_nr;
    
        last_pkmap_nr = (last_pkmap_nr + 1) & LAST_PKMAP_MASK;
        return last_pkmap_nr;
    }

    该计数器计算了内核使用该页的次数加1. 如果计数器值为2, 则内核中只有一处使用该映射页. 计数器值为5表示有4处使用. 一般地说,计数器值为n代表内核中有n-1处使用该页. 和通常的使用计数器一样, 0意味着相关的页没有使用.计数器值1有特殊语义. 这表示该位置关联的页已经映射, 但由于CPU的TLB没有更新而无法使用, 此时访问该页, 或者失败, 或者会访问到一个不正确的地址

    为了记录高端内存页框与永久内核映射包含的线性地址之间的联系,内核使用了page_address_htable散列表. 该表包含一个page_address_map数据结构,用于为高端内存中的每一个页框进行当前映射。而该数据结构还包含一个指向页描述符的指针和分配给该页框的线性地址。

    内核利用page_address_map数据结构, 来建立物理内存页的page实例与其在虚似内存区中位置之间的关联.

    /*
     * Describes one page->virtual association
     */
    struct page_address_map
    {
        struct page *page;
        void *virtual;
        struct list_head list;
    };

    该结构用于建立page-->virtual的映射(该结构由此得名).

    字段描述
    page是一个指向全局mem_map数组中的page实例的指针
    virtual指定了该页在内核虚拟地址空间中分配的位置

    为便于组织, 映射保存在散列表中, 结构中的链表元素用于建立溢出链表,以处理散列碰撞. 该散列表通过page_address_htable数组实现, 定义在mm/highmem.c?v=4.7, line 392

    /*
     * Hash table bucket
     */
    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];

    散列表的散列函数是page_slot函数, 定义在mm/highmem.c?v=4.7, line 397

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

    2.2 page_address函数


    page_address是一个前端函数, 使用上述数据结构确定给定page实例的线性地址, 该函数定义在mm/highmem.c?v=4.7, line 408)

    /**
     * page_address - get the mapped virtual address of a page
     * @page: &struct page to get the virtual address of
     *
     * Returns the page's virtual address.
     */
    void *page_address(const struct page *page)
    {
        unsigned long flags;
        void *ret;
        struct page_address_slot *pas;
        /*如果页框不在高端内存中*/  
        if (!PageHighMem(page))
             /*线性地址总是存在,通过计算页框下标 
                然后将其转换成物理地址,最后根据相应的 
                /物理地址得到线性地址*/
            return lowmem_page_address(page);
        /*从page_address_htable散列表中得到pas*/  
        pas = page_slot(page);
        ret = NULL;
        spin_lock_irqsave(&pas->lock, flags);
        if (!list_empty(&pas->lh)) {{/*如果对应的链表不空, 
        该链表中存放的是page_address_map结构*/  
            struct page_address_map *pam;
            /*对每个链表中的元素*/
            list_for_each_entry(pam, &pas->lh, list) {
                if (pam->page == page) {
                    /*返回线性地址*/ 
                    ret = pam->virtual;
                    goto done;
                }
            }
        }
    done:
        spin_unlock_irqrestore(&pas->lock, flags);
        return ret;
    }
    
    EXPORT_SYMBOL(page_address);

    page_address首先检查传递进来的page实例在普通内存还是在高端内存.

    • 如果是前者(普通内存区域), 页地址可以根据pagemem_map数组中的位置计算. 这个工作可以通过lowmem_page_address调用page_to_virt(page)来完成

    • 对于后者, 可通过上述散列表查找虚拟地址.

    2.3 kmap创建映射


    2.3.1 kmap函数


    为通过page指针建立映射, 必须使用kmap函数.

    不同体系结构的定义可能不同, 但是大多数体系结构的定义都如下所示, 比如arm上该函数定义在arch/arm/mm/highmem.c?v=4.7, line 37, 如下所示

    /*高端内存映射,运用数组进行操作分配情况 
    分配好后需要加入哈希表中;*/  
    void *kmap(struct page *page)
    {
        might_sleep();
        if (!PageHighMem(page)) /*如果页框不属于高端内存*/  
            return page_address(page);
        return kmap_high(page); /*页框确实属于高端内存*/  
    }
    EXPORT_SYMBOL(kmap);

    kmap函数只是一个page_address的前端,用于确认指定的页是否确实在高端内存域中. 否则, 结果返回page_address得到的地址. 如果确实在高端内存中, 则内核将工作委托给kmap_high

    kmap_high的实现在函数mm/highmem.c?v=4.7, line 275中, 定义如下

    2.3.2 kmap_high函数


    /**
     * kmap_high - map a highmem page into memory
     * @page: &struct page to map
     *
     * Returns the page's virtual memory address.
     *
     * We cannot call this from interrupts, as it may block.
     */
    void *kmap_high(struct page *page)
    {
        unsigned long vaddr;
    
        /*
         * For highmem pages, we can't trust "virtual" until
         * after we have the lock.
         */
        lock_kmap();    /*保护页表免受多处理器系统上的并发访问*/  
    
        /*检查是否已经被映射*/
        vaddr = (unsigned long)page_address(page);
        if (!vaddr) )/*  如果没有被映射  */    
            /*把页框的物理地址插入到pkmap_page_table的 
            一个项中并在page_address_htable散列表中加入一个 
            元素*/  
            vaddr = map_new_virtual(page);
        /*分配计数加一,此时流程都正确应该是2了*/  
        pkmap_count[PKMAP_NR(vaddr)]++;
        BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
        unlock_kmap();
        return (void*) vaddr;   ;/*返回地址*/ 
    }
    
    EXPORT_SYMBOL(kmap_high);

    2.3.3 map_new_virtual函数


    上文讨论的page_address函数首先检查该页是否已经映射. 如果它不对应到有效地址, 则必须使用map_new_virtual映射该页.

    该函数定义在mm/highmem.c?v=4.7, line 213, 将执行下列主要的步骤.

    static inline unsigned long map_new_virtual(struct page *page)
    {
        unsigned long vaddr;
        int count;
        unsigned int last_pkmap_nr;
        unsigned int color = get_pkmap_color(page);
    
    start:
        count = get_pkmap_entries_count(color);
        /* Find an empty entry */
        for (;;) {
            last_pkmap_nr = get_next_pkmap_nr(color);   /*加1,防止越界*/  
            /* 接下来判断什么时候last_pkmap_nr等于0,等于0就表示1023(LAST_PKMAP(1024)-1)个页表项已经被分配了 
            ,这时候就需要调用flush_all_zero_pkmaps()函数,把所有pkmap_count[] 计数为1的页表项在TLB里面的entry给flush掉 
            ,并重置为0,这就表示该页表项又可以用了,可能会有疑惑为什么不在把pkmap_count置为1的时候也 
            就是解除映射的同时把TLB也flush呢? 
            个人感觉有可能是为了效率的问题吧,毕竟等到不够的时候再刷新,效率要好点吧。*/  
            if (no_more_pkmaps(last_pkmap_nr, color)) {
                flush_all_zero_pkmaps();
                count = get_pkmap_entries_count(color);
            }
    
            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);
                wait_queue_head_t *pkmap_map_wait =
                    get_pkmap_wait_queue_head(color);
    
                __set_current_state(TASK_UNINTERRUPTIBLE);
                add_wait_queue(pkmap_map_wait, &wait);
                unlock_kmap();
                schedule();
                remove_wait_queue(pkmap_map_wait, &wait);
                lock_kmap();
    
                /* Somebody else might have mapped it while we slept */
                if (page_address(page))
                    return (unsigned long)page_address(page);
    
                /* Re-start */
                goto start;
            }
        }
        /*返回这个页表项对应的线性地址vaddr.*/  
        vaddr = PKMAP_ADDR(last_pkmap_nr);
        /*设置页表项*/  
        set_pte_at(&init_mm, vaddr,
               &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));
        /*接下来把pkmap_count[last_pkmap_nr]置为1,1不是表示不可用吗, 
        既然映射已经建立好了,应该赋值为2呀,其实这个操作 
        是在他的上层函数kmap_high里面完成的(pkmap_count[PKMAP_NR(vaddr)]++).*/  
        pkmap_count[last_pkmap_nr] = 1;
        /*到此为止,整个映射就完成了,再把page和对应的线性地址 
        加入到page_address_htable哈希链表里面就可以了*/  
        set_page_address(page, (void *)vaddr);
    
        return vaddr;
    }
    1. 从最后使用的位置(保存在全局变量last_pkmap_nr中)开始,反向扫描pkmap_count数组, 直至找到一个空闲位置. 如果没有空闲位置,该函数进入睡眠状态,直至内核的另一部分执行解除映射操作腾出空位. 在到达pkmap_count的最大索引值时, 搜索从位置0开始. 在这种情况下, 还调用
      flush_all_zero_pkmaps函数刷出CPU高速缓存(读者稍后会看到这一点)。

    2. 修改内核的页表,将该页映射在指定位置。但尚未更新TLB.

    3. 新位置的使用计数器设置为1。如上所述,这意味着该页已分配但无法使用,因为TLB项未更新.

    4. set_page_address将该页添加到持久内核映射的数据结构。
      该函数返回新映射页的虚拟地址. 在不需要高端内存页的体系结构上(或没有设置CONFIG_HIGHMEM),则使用通用版本的kmap返回页的地址,且不修改虚拟内存

    2.4 kunmap解除映射


    kmap映射的页, 如果不再需要, 必须用kunmap解除映射. 照例, 该函数首先检查相关的页(由page实例标识)是否确实在高端内存中. 倘若如此, 则实际工作委托给mm/highmem.c中的kunmap_high, 该函数的主要任务是将pkmap_count数组中对应位置在计数器减1

    该机制永远不能将计数器值降低到小于1. 这意味着相关的页没有释放。因为对使用计数器进行了额外的加1操作, 正如前文的讨论, 这是为确保CPU高速缓存的正确处理.

    也在上文提到的flush_all_zero_pkmaps是最终释放映射的关键. 在map_new_virtual从头开始搜索空闲位置时, 总是调用该函数.

    它负责以下3个操作。

    1. flush_cache_kmaps在内核映射上执行刷出(在需要显式刷出的大多数体系结构上,将使用flush_cache_all刷出CPU的全部的高速缓存), 因为内核的全局页表已经修改.

    2. 扫描整个pkmap_count数组. 计数器值为1的项设置为0,从页表删除相关的项, 最后删除该映射。

    3. 最后, 使用flush_tlb_kernel_range函数刷出所有与PKMAP区域相关的TLB项.

    2.4.1 kunmap函数


    同kmap类似, 每个体系结构都应该实现自己的kmap函数, 大多数体系结构的定义都如下所示, 参见arch/arm/mm/highmem.c?v=4.7, line 46

    void kunmap(struct page *page)
    {
        BUG_ON(in_interrupt());
        if (!PageHighMem(page))
            return;
        kunmap_high(page);
    }
    EXPORT_SYMBOL(kunmap);

    内核首先检查待释放内存区域是不是在高端内存区域

    • 如果内存区域在普通内存区, 则内核并没有通过kmap_high对其建立持久的内核映射, 当然也无需用kunmap_high释放

    • 如果内存区域在高端内存区, 则内核通过kunmap_high释放该内存空间

    2.4.2 kunmap_high函数


    kunmap_high函数定义在mm/highmem.c?v=4.7, line 328

    #ifdef CONFIG_HIGHMEM
    /**
     * kunmap_high - unmap a highmem page into memory
     * @page: &struct page to unmap
     *
     * If ARCH_NEEDS_KMAP_HIGH_GET is not defined then this may be called
     * only from user context.
     */
    void kunmap_high(struct page *page)
    {
        unsigned long vaddr;
        unsigned long nr;
        unsigned long flags;
        int need_wakeup;
        unsigned int color = get_pkmap_color(page);
        wait_queue_head_t *pkmap_map_wait;
    
        lock_kmap_any(flags);
        vaddr = (unsigned long)page_address(page);
        BUG_ON(!vaddr);
        nr = PKMAP_NR(vaddr);   /*永久内存区域开始的第几个页面*/  
    
        /*
         * A count must never go down to zero
         * without a TLB flush!
         */
        need_wakeup = 0;
        switch (--pkmap_count[nr]) {    /*减小这个值,因为在映射的时候对其进行了加2*/  
        case 0:
            BUG();
        case 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.
             */
            pkmap_map_wait = get_pkmap_wait_queue_head(color);
            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);
    }
    
    EXPORT_SYMBOL(kunmap_high);
    #endif

    3 临时内核映射


    刚才描述的kmap函数不能用于中断处理程序, 因为它可能进入睡眠状态. 如果pkmap数组中没有空闲位置, 该函数会进入睡眠状态, 直至情形有所改善.

    因此内核提供了一个备选的映射函数, 其执行是原子的, 逻辑上称为kmap_atomic. 该函数的一个主要优点是它比普通的kmap快速. 但它不能用
    于可能进入睡眠的代码. 因此, 它对于很快就需要一个临时页的简短代码,是非常理想的.

    kmap_atomic的定义在IA-32, PPC, Sparc32上是特定于体系结构的, 但这3种实现只有非常细微的差别. 其原型是相同的.

    3.1 kmap_atomic函数


    //  http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=4.7#L55
    void *kmap_atomic(struct page *page)

    page是一个指向高端内存页的管理结构的指针, 而早期的内核中, 增加了一个类型为enum km_typetype参数, 用于指定所需的映射类型

    //  http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=2.6.32#L39
    void *kmap_atomic(struct page *page, enum km_type type)

    而在新的内核中, 删除了这个标识, 但是保留了km_type的最大值KM_TYPE_NR

    void *kmap_atomic(struct page *page)
    {
        unsigned int idx;
        unsigned long vaddr;
        void *kmap;
        int type;
    
        preempt_disable();
        pagefault_disable();
        if (!PageHighMem(page))
            return page_address(page);
    
    #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 = kmap_high_get(page);
        if (kmap)
            return kmap;
    
        type = kmap_atomic_idx_push();
    
        idx = FIX_KMAP_BEGIN + type + KM_TYPE_NR * smp_processor_id();
        vaddr = __fix_to_virt(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(get_fixmap_pte(vaddr)));
    #endif
        /*
         * When debugging is off, kunmap_atomic leaves the previous mapping
         * in place, so the contained TLB flush ensures the TLB is updated
         * with the new mapping.
         */
        set_fixmap_pte(idx, mk_pte(page, kmap_prot));
    
        return (void *)vaddr;
    }
    EXPORT_SYMBOL(kmap_atomic);

    这个函数不会被阻塞, 因此可以用在中断上下文和起亚不能重新调度的地方. 它也禁止内核抢占, 这是有必要的, 因此映射对每个处理器都是唯一的(调度可能对哪个处理器执行哪个进程做变动).

    3.2 kunmap_atomic函数


    可以通过函数kunmap_atomic取消映射

    /*
     * Prevent people trying to call kunmap_atomic() as if it were kunmap()
     * kunmap_atomic() should get the return value of kmap_atomic, not the page.
     */
    #define kunmap_atomic(addr)                     \
    do {                                \
        BUILD_BUG_ON(__same_type((addr), struct page *));       \
        __kunmap_atomic(addr);                  \
    } while (0)

    这个函数也不会阻塞. 在很多体系结构中, 除非激活了内核抢占, 否则kunmap_atomic根本无事可做, 因为只有在下一个临时映射到来前上一个临时映射才有效. 因此, 内核完全可以”忘掉”kmap_atomic映射, kunmap_atomic也无需做什么实际的事情. 下一个原子映射将自动覆盖前一个映射.

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    展开全文
  • 日期 内核版本 架构 作者 GitHub CSDN 2016-09-29 ... 在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. Linux内核

    原文链接:https://blog.csdn.net/gatieme/article/details/52705178

    日期内核版本架构作者GitHubCSDN
    2016-09-29Linux-4.7X86 & armgatiemeLinuxDeviceDriversLinux内存管理

    在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法.

    Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述.

    伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者.

    • 内核如何记住哪些内存块是空闲的

    • 分配空闲页面的方法

    • 影响分配器行为的众多标识位

    • 内存碎片的问题和分配器如何处理碎片

    1 高端内存与内核映射


    尽管vmalloc函数族可用于从高端内存域向内核映射页帧(这些在内核空间中通常是无法直接看到的), 但这并不是这些函数的实际用途.

    重要的是强调以下事实 : 内核提供了其他函数用于将ZONE_HIGHMEM页帧显式映射到内核空间, 这些函数与vmalloc机制无关. 因此, 这就造成了混乱.

    而在高端内存的页不能永久地映射到内核地址空间. 因此, 通过alloc_pages()函数以__GFP_HIGHMEM标志获得的内存页就不可能有逻辑地址.

    在x86_32体系结构总, 高于896MB的所有物理内存的范围大都是高端内存, 它并不会永久地或自动映射到内核地址空间, 尽管X86处理器能够寻址物理RAM的范围达到4GB(启用PAE可以寻址64GB), 一旦这些页被分配, 就必须映射到内核的逻辑地址空间上. 在x86_32上, 高端地址的页被映射到内核地址空间(即虚拟地址空间的3GB~4GB)

    内核地址空间的最后128 MiB用于何种用途呢?

    该部分有3个用途。

    1. 虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配. 该机制通常用于用户过程, 内核自身会试图尽力避免非连续的物理地址。内核通常会成功,因为大部分大的内存块都在启动时分配给内核,那时内存的碎片尚不严重。但在已经运行了很长时间的系统上, 在内核需要物理内存时, 就可能出现可用空间不连续的情况. 此类情况, 主要出现在动态加载模块时.

    2. 持久映射用于将高端内存域中的非持久页映射到内核中

    3. 固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以自由选择. 它与通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间的关联可以自行定义,关联建立后内核总是会注意到的.

    x86_32上的地址划分

    在这里有两个预处理器符号很重要 __VMALLOC_RESERVE设置了vmalloc区域的长度, 而MAXMEM则表示内核可以直接寻址的物理内存的最大可能数量.

    内核虚拟地址空间

    内核中, 将内存划分为各个区域是通过图3-15所示的各个常数控制的。根据内核和系统配置, 这些常数可能有不同的值。直接映射的边界由high_memory指定。

    1. 直接映射区
      线性空间中从3G开始最大896M的区间, 为直接内存映射区,该区域的线性地址和物理地址存在线性转换关系:线性地址=3G+物理地址。

    2. 动态内存映射区
      该区域由内核函数vmalloc来分配, 特点是 : 线性空间连续, 但是对应的物理空间不一定连续. vmalloc分配的线性地址所对应的物理页可能处于低端内存, 也可能处于高端内存.

    3. 永久内存映射区
      该区域可访问高端内存. 访问方法是使用alloc_page(_GFP_HIGHMEM)分配高端内存页或者使用kmap函数将分配到的高端内存映射到该区域.

    4. 固定映射区
      该区域和4G的顶端只有4k的隔离带,其每个地址项都服务于特定的用途,如ACPI_BASE等。

    说明

    注意用户空间当然可以使用高端内存,而且是正常的使用,内核在分配那些不经常使用的内存时,都用高端内存空间(如果有),所谓不经常使用是相对来说的,比如内核的一些数据结构就属于经常使用的,而用户的一些数据就属于不经常使用的。用户在启动一个应用程序时,是需要内存的,而每个应用程序都有3G的线性地址,给这些地址映射页表时就可以直接使用高端内存。

    而且还要纠正一点的是:那128M线性地址不仅仅是用在这些地方的,如果你要加载一个设备,而这个设备需要映射其内存到内核中,它也需要使用这段线性地址空间来完成,否则内核就不能访问设备上的内存空间了.

    总之,内核的高端线性地址是为了访问内核固定映射以外的内存资源。进程在使用内存时,触发缺页异常,具体将哪些物理页映射给用户进程是内核考虑的事情. 在用户空间中没有高端内存这个概念.

    即内核对于低端内存, 不需要特殊的映射机制, 使用直接映射即可以访问普通内存区域, 而对于高端内存区域, 内核可以采用三种不同的机制将页框映射到高端内存 : 分别叫做永久内核映射临时内核映射以及非连续内存分配

    2 固定映射


    2.1 数据结构


    linux高端内存中的临时内存区为固定内存区的一部分, 对于固定内存在linux内核中有下面描述

    x86armarm64
    arch/x86/include/asm/fixmap.h?v=4.7, line 67arch/arm/include/asm/fixmap.h?v=4.7, line 11arch/arm64/include/asm/fixmap.h?v=4.7, line 36
    /*
     * Here we define all the compile-time 'special' virtual
     * addresses. The point is to have a constant address at
     * compile time, but to set the physical address only
     * in the boot process.
     *
     * These 'compile-time allocated' memory buffers are
     * page-sized. Use set_fixmap(idx,phys) to associate
     * physical memory with fixmap indices.
     *
     */
    enum fixed_addresses {
        FIX_HOLE,
    
        /*
         * Reserve a virtual window for the FDT that is 2 MB larger than the
         * maximum supported size, and put it at the top of the fixmap region.
         * The additional space ensures that any FDT that does not exceed
         * MAX_FDT_SIZE can be mapped regardless of whether it crosses any
         * 2 MB alignment boundaries.
         *
         * Keep this at the top so it remains 2 MB aligned.
         */
    #define FIX_FDT_SIZE        (MAX_FDT_SIZE + SZ_2M)
        FIX_FDT_END,
        FIX_FDT = FIX_FDT_END + FIX_FDT_SIZE / PAGE_SIZE - 1,
    
        FIX_EARLYCON_MEM_BASE,
        FIX_TEXT_POKE0,
        __end_of_permanent_fixed_addresses,
    
        /*
         * Temporary boot-time mappings, used by early_ioremap(),
         * before ioremap() is functional.
         */
    #define NR_FIX_BTMAPS       (SZ_256K / PAGE_SIZE)
    #define FIX_BTMAPS_SLOTS    7
    #define TOTAL_FIX_BTMAPS    (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)
    
        FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
        FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1,
    
        /*
         * Used for kernel page table creation, so unmapped memory may be used
         * for tables.
         */
        FIX_PTE,
        FIX_PMD,
        FIX_PUD,
        FIX_PGD,
    
        __end_of_fixed_addresses
    };

    2.2 固定映射


    ioremap的作用是将IOBIOS以及物理地址空间映射到在896M至1G的128M的地址空间内, 使得kernel能够访问该空间并进行相应的读写操作。

    start_kernel()->setup_arch()->early_ioremap_init()

    然后arm和arm64上early_ioremap_init又是early_ioremap_setup的前端

    函数x86armarm64
    early_ioremap_initarch/x86/mm/ioremap.c?v=4.7, line 445arch/arm/mm/ioremap.c?v=4.7, line 489arch/arm64/mm/ioremap.c?v=4.7, line 110
    early_ioremap_setupmm/early_ioremap.c?v=4.7, line 67体系结构无关体系结构无关

    其中arm和arm64架构下的early_ioremap_init函数实现比较简单, 都是直接的early_ioremap_setup函数的前端

    /*
     * Must be called after early_fixmap_init
     */
    void __init early_ioremap_init(void)
    {
        early_ioremap_setup();
    }

    但是arm和arm64下的setup_arch函数则会先调用early_fixmap_init函数来填充fixmap. 参见arch/arm/kernel/setup.c?v=4.7, line 1058arch/arm64/kernel/setup.c?v=4.7, line 229.

    void __init setup_arch(char **cmdline_p)
    {
        early_fixmap_init();
        early_ioremap_init();
    }

    early_fixmap_init函数的定义在

    armarm64
    arch/arm/mm/mmu.c?v=4.7, line 385arch/arm64/mm/mmu.c?v=4.7, line 676

    其中arm架构的定义如下所示, 在arch/arm/mm/mmu.c?v=4.7, line 385

    void __init early_fixmap_init(void)
    {
        pmd_t *pmd;
    
        /*
         * The early fixmap range spans multiple pmds, for which
         * we are not prepared:
         */
        BUILD_BUG_ON((__fix_to_virt(__end_of_early_ioremap_region) >> PMD_SHIFT)
                 != FIXADDR_TOP >> PMD_SHIFT);
    
        /*得到固定映射区的pmd
        ,此pmd为虚拟地址转换为物理地址的pmd*/
        pmd = fixmap_pmd(FIXADDR_TOP);
         /*将bm_pte页表设置为固定映射区开始地址的pmd的第一个页表;*/
        pmd_populate_kernel(&init_mm, pmd, bm_pte);
    
        pte_offset_fixmap = pte_offset_early_fixmap;
    }

    随后setup_arch中调用early_ioremap_setup函数将fixed_address里的索引的虚拟地址放入slot_virt, 参见mm/early_ioremap.c?v=4.7, line 63

    static void __iomem *prev_map[FIX_BTMAPS_SLOTS] __initdata;
    static unsigned long prev_size[FIX_BTMAPS_SLOTS] __initdata;
    static unsigned long slot_virt[FIX_BTMAPS_SLOTS] __initdata;
    
    void __init early_ioremap_setup(void)
    {
        int i;
    
        for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
            if (WARN_ON(prev_map[i]))
                break;
        /*  将fixed_address里的索引的虚拟地址放入slot_virt
             从代码里面可以看出,放入slot_virt中得虚拟地址为1M  */
        for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
            slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
    }

    而x86下的没有定义early_fixmap_init函数, 因此在early_ioremap_init函数中完成了fixmap的初始化工作, 定义在arch/x86/mm/ioremap.c?v=4.7, line 445

    2.3 ioremap函数


    对于ioremap的使用需要通过early_memremapearly_iounmap进行.

    由于对应于ioremap的内存空间是有限的, 所以对于ioremap空间的使用遵照使用结束马上释放的原则. 这就是说early_memremapearly_iounmap必须配对使用并且访问结束必须马上执行unmap

    3 临时内核映射


    刚才描述的kmap函数不能用于中断处理程序, 因为它可能进入睡眠状态. 如果pkmap数组中没有空闲位置, 该函数会进入睡眠状态, 直至情形有所改善.

    因此内核提供了一个备选的映射函数, 其执行是原子的, 逻辑上称为kmap_atomic. 该函数的一个主要优点是它比普通的kmap快速. 但它不能用
    于可能进入睡眠的代码. 因此, 它对于很快就需要一个临时页的简短代码,是非常理想的.

    kmap_atomic的定义在IA-32, PPC, Sparc32上是特定于体系结构的, 但这3种实现只有非常细微的差别. 其原型是相同的.

    3.1 kmap_atomic函数


    //  http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=4.7#L55
    void *kmap_atomic(struct page *page)

    page是一个指向高端内存页的管理结构的指针, 而早期的内核中, 增加了一个类型为enum km_typetype参数, 用于指定所需的映射类型

    //  http://lxr.free-electrons.com/source/arch/arm/mm/highmem.c?v=2.6.32#L39
    void *kmap_atomic(struct page *page, enum km_type type)

    而在新的内核中, 删除了这个标识, 但是保留了km_type的最大值KM_TYPE_NR

    void *kmap_atomic(struct page *page)
    {
        unsigned int idx;
        unsigned long vaddr;
        void *kmap;
        int type;
    
        preempt_disable();
        pagefault_disable();
        if (!PageHighMem(page))
            return page_address(page);
    
    #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 = kmap_high_get(page);
        if (kmap)
            return kmap;
    
        type = kmap_atomic_idx_push();
    
        idx = FIX_KMAP_BEGIN + type + KM_TYPE_NR * smp_processor_id();
        vaddr = __fix_to_virt(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(get_fixmap_pte(vaddr)));
    #endif
        /*
         * When debugging is off, kunmap_atomic leaves the previous mapping
         * in place, so the contained TLB flush ensures the TLB is updated
         * with the new mapping.
         */
        set_fixmap_pte(idx, mk_pte(page, kmap_prot));
    
        return (void *)vaddr;
    }
    EXPORT_SYMBOL(kmap_atomic);

    这个函数不会被阻塞, 因此可以用在中断上下文和起亚不能重新调度的地方. 它也禁止内核抢占, 这是有必要的, 因此映射对每个处理器都是唯一的(调度可能对哪个处理器执行哪个进程做变动).

    3.2 kunmap_atomic函数


    可以通过函数kunmap_atomic取消映射

    /*
     * Prevent people trying to call kunmap_atomic() as if it were kunmap()
     * kunmap_atomic() should get the return value of kmap_atomic, not the page.
     */
    #define kunmap_atomic(addr)                     \
    do {                                \
        BUILD_BUG_ON(__same_type((addr), struct page *));       \
        __kunmap_atomic(addr);                  \
    } while (0)

    这个函数也不会阻塞. 在很多体系结构中, 除非激活了内核抢占, 否则kunmap_atomic根本无事可做, 因为只有在下一个临时映射到来前上一个临时映射才有效. 因此, 内核完全可以”忘掉”kmap_atomic映射, kunmap_atomic也无需做什么实际的事情. 下一个原子映射将自动覆盖前一个映射.

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    展开全文
  • )描述了高端内存原理和源码注释详解,能够清楚的理解什么是高端内存,什么是vmalloc,什么是永久映射,什么是临时映射,需要注意什么,应用场合是怎样,在源码中的前龙后脉都有详细的注释和解释
  • Linux高端内存映射

    2019-05-29 20:50:56
    文章目录高端内存的由来物理内存映射永久内核映射临时内核映射非连续内存分配 高端内存的由来 通常32位Linux内核地址空间划分0 ~ 3G为用户空间,3 ~ 4G为内核空间 当内核模块代码或线程访问内存时,代码中的内存...

    物理内存的划分

    Linux把每个物理内存划分成3个管理区

    内存区域说明
    ZONE_DMA范围是0~16M,该区域的物理页面专门供I/O设备的DMA使用
    ZONE_NORMAL范围是16~896M,该区域的物理页面是内核能够直接使用的
    ZONE_HIGHMEM范围是896~结束,高端内存,内核不能直接使用

    高端内存的由来

    通常32位Linux内核地址空间划分0 ~ 3G为用户空间,3 ~ 4G为内核空间
    在这里插入图片描述
    当内核模块代码或线程访问内存时,代码中的内存地址都为逻辑地址而对应到真正的物理内存地址,需要地址一对一的映射。

    物理地址 = 逻辑地址 – 0xC0000000

    假设按照上述简单的地址映射关系,内核只能访问1G物理内存空间

    存在这样一个问题由于内核只能访问1G物理内存空间,如果物理内存是2G,那内核如何访问剩余的1G物理内存空间呢?

    由本文开始我们知道物理内存地址被划分成了3个Zone,其中0~ 896M是内核直接可以访问的,896M ~ 2G这一部分内核要如何访问呢?

    实际上内核想访问高于896M的物理地址时,从0xF8000000~ 0xFFFFFFFF虚拟地址空间中取一部分与想要访问的物理内存建立映射即填充内核PTE页表(内核页表),访问完成之后内核释放0xF8000000~0xFFFFFFFF中的虚拟地址空间,以便其它进程访问。(采用这128M的虚拟空间,建立临时地址映射,完成了对所有高于896M物理内存的访问)

    如下图所示

    1G的虚拟内核空间如下图
    在这里插入图片描述
    上图中PAGE_OFFSET通常为0xC000 0000,而high_memory指的是0xF7FF FFFF,在物理内存映射区和和第一个vmalloc区之间插入的8MB的内存区是一个安全区,其目的是为了“捕获”对内存的越界访问。处于同样的理由,插入其他4KB大小的安全区来隔离非连续的内存区。

    内核空间的映射

    Linux内核可以采用以下机制将页框映射到内核空间

    虚拟内存区域说明
    物理内存映射映射物理内存空间0~896M部分,这一部分是内核可以直接访问的。
    vmalloc区用来分配物理地址非连续的内存空间
    永久内核映射允许内核建立高端页框到内核虚拟地址空间的长期映射。
    固定映射的线性地址空间其中的一部分用于建立临时内核映射

    物理内存映射

    线性空间中从3G开始大小为896M的区间,为直接内存映射区

    映射关系为:物理地址等于虚拟地址减去0xc0000000,即虚拟地址减去3G。

    永久内核映射

    如果是通过 alloc_page() 获得了高端内存对应的 page,如何给它找个线性空间?

    内核专门为此留出一块线性空间,从 PKMAP_BASE 到 FIXADDR_START ,用于映射高端内存。这个空间叫永久内核映射空间

    这个空间和其它空间使用同样的页目录表,对于内核来说,就是 swapper_pg_dir,对普通进程来说,通过 CR3 寄存器指向。通常情况下,这个空间是 4M 大小,因此仅仅需要一个页表即可,在内核初始化页表管理机制时,专门用pkmap_page_table这个变量保存了PKMAP_BASE对应的页表项的地址,由pkmap_page_table来维护永久内核映射区的页表项的映射,页表项总数为LAST_PKMAP个。通过 kmap(),可以把一个 page 映射到这个空间来。

    由于这个空间是 4M 大小,最多能同时映射 1024 个 page。因此,对于不使用的的 page,应该及时通过 kunmap()把一个 page 对应的线性地址从这个空间释放掉(也就是解除映射关系)

    这里的永久并不是指调用kmap()建立的映射关系会一直持续下去无法解除,而是指在调用kunmap()解除映射之间这种映射会一直存在,这是相对于临时内核映射机制而言的。

    需要注意的是,当永久内核映射区没有空闲的页表项可供映射时,请求映射的进程会被阻塞,因此永久内核映射请求不能发生在中断和可延迟函数中。

    临时内核映射

    临时内核映射和永久内核映射相比,其最大的特点就是不会阻塞请求映射页框的进程,因此临时内核映射请求可以发生在中断和可延迟函数中

    这块空间具有如下特点:
    (1)每个 CPU 占用一块空间
    (2)在每个CPU 占用的那块空间中,又分为多个小空间,每个小空间大小是 1 个 page,每个小空间用于一个目的,这些目的定义在 kmap_types.h 中的 km_type 中。

    系统中的每个CPU都有自己的13个临时内核映射窗口,根据不同的需求(用于内核控制路径),选择不同的窗口来创建映射。每个CPU的映射窗口集合用 enum km_type 数据结构表示,该数据结构中的每个符号,如 KM_BOUNCE_READ 、 KM_USER0 或 KM_PTE0 ,标识了窗口的线性地址,其实是一个下标。当要内核建立一个临时映射时,通过 cpu_id 和 窗口下标 来确定线性地址。通过 kmap_atomic() 可实现临时映射。

    临时内核映射的实现比永久内核映射要简单,当一个进程申请在某个窗口创建映射,即使这个窗口已经在之前就建立了映射,新的映射也会建立并且覆盖之前的映射,所以说这种映射机制是临时的,并且不会阻塞当前进程。

    非连续内存分配

    非连续内存分配是指将物理地址不连续的页框映射到线性地址连续的线性地址空间

    主要应用于大容量的内存分配。
    优点避免了外部碎片
    缺点必须打乱内核页表,而且访问速度较连续分配的物理页框慢

    非连续内存分配的线性地址空间是从 VMALLOC_START 到 VMALLOC_END ,每当内核要用vmalloc类的函数进行非连续内存分配,就会申请一个vm_struct结构来描述对应的vmalloc区,两个vmalloc区之间的间隔至少为一个页框的大小,即PAGE_SIZE。

    总结

    • 由于内核的线性地址空间有限,因此采取上面介绍的三种方式来映射高端内存。但是每种映射的本质都是通过页表来建立线性地址与物理地址之间的联系。

    • 永久内核映射和临时内核映射,都由内核指定了需要进行映射的页面,也就是说指定了页描述符(页描述符和物理页框之间的关系是固定不可变的)。
      在永久内核映射中,内核只需要在永久内核映射区找到空闲的,也就是未被映射的线性地址对应的页表项,然后将其分配给page即可,若找不到则将阻塞申请建立映射的进程。
      而临时内核映射更直接,连进行映射的线性地址窗口都是固定的,若是其已经分配给了某个页框,则直接抢过来用,因此之前的映射就被覆盖了,体现出了临时性

    • 非连续内存分配,内核不用指定具体的页框,只需指定要申请的内存大小,内核将在非连续内存分配区找到一块相应大小虚拟地址空间,然后再由伙伴系统分配页框,还要通过slab分配器为一些数据结构分配内存,最后再用同样的方式(设置PTE表项)来建立映射。

    展开全文
  • 1.1 内核地址空间(线性空间)分布(1)直接映射区:线性空间中从3G开始最大896M的区间,为直接内存映射区,该...vmalloc分配的线性地址所对应的物理页可能处于低端内存,也可能处于高端内存。(3)永久内存映射区:该区...
  • 高端内存含义为:线性地址空间 PAGE_OFFSET + 896M 至4G的最后128M线性地址  896M以上的物理页框,非直接映射。有3种方法:非连续内存区映射,永久内核映射,临时内核映射(固定映射)  从 PAGE_OFFSET开始的线性...
  • 我们知道系统中的高端内存是不能作为直接映射映射到内核空间的,那么我们想要使用它怎么办呢?前面的文章我们已经有过相关的介绍,可以使用三种方法,分别是pkmap(永久映射)/fixmap(临时固定映射)/vmlloc,...
  • linux高端内存映射

    千次阅读 2014-03-03 16:23:08
    用户空间:在Linux中,每个用户进程都可以访问4GB的线性虚拟内存空间。其中从0到3GB的虚存地址是用户空间,通过每个进程自己的页目录、页表,用户进程可以直接访问。 内核空间:从3GB到4GB的虚存地址为内核态空间,...
  • 一:引子 我们在前面分析过,在linux...通常,我们把物理地址超过896M的区域称为高端内存。内核怎样去管理高端内存呢?今天就来分析这个问题。 内核有三种方式管理高端内存。第一种是非连续映射。这我们在前面的vmalloc
  • Linux高端内存映射(上)

    2015-10-19 10:16:18
    高端内存概述  在32位的系统上,内核占有从第3GB~第4GB的...引入高端内存映射这样一个概念的主要原因就是我们所安装的内存大于1G时,内核的1G线性地址空间无法建立一个完全的直接映射来触及整个物理内存空间,而对
  • 原文:linux内核笔记之高端内存映射   在32位的系统上,内核使用第3GB~第4GB的线性地址空间,共1GB大小。内核将其中的前896MB与物理内存的0~896MB进行直接映射,即线性映射,将剩余的128M线性地址空间作为访问...
  • 高端内存映射

    2011-07-02 20:52:10
    1 内核空间和用户空间 用户空间:在Linux中,每个用户进程都可以访问4GB的线性虚拟内存空间。其中从0到3GB的虚存地址是用户空间,通过每个进程自己的页目录、页表,用户进程可以直接访问。内核空间:从3GB到4GB的...
  • 日期 内核版本 架构 作者 GitHub ...1 前景回顾1.1 内核映射区尽管vmalloc函数族可用于从高端内存域向内核映射页帧(这些在内核空间中通常是无法直接看到的), 但这并不是这些函数的实际用途.重要
  • 与直接映射的物理内存末端、高端内存的始端所对应的线性地址存放在high_memory变量中,在x86体系结构上,高于896MB的所有物理内存的范围大都是高端内存,它并不会地或自动地映射到内核地址空间,尽管x86处理器能够...
  • linux高端内存内存映射——重要

    千次阅读 2018-04-17 20:20:25
    Linux 操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能简单地使用指针传递数据,因为Linux使用的虚拟内存机制,用户空间的数据可能被换出,当内核空间使用用户空间指针时,对应的数据可能不在...
  • Linux 操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能简单地使用指针传递数据,因为Linux使用的虚拟内存机制,用户空间的数据可能被...Linux内核高端内存的由来当内核模块代码或线程访问内存时,
  • 一、永久映射和临时映射: 2.1、永久映射: 依然把前面描述vmalloc的文章的图搬上来: high_memory VMALLOC_START Vmalloc 1 4KB...
  • 1.1 内核地址空间(线性空间)分布   (1) 直接映射区:线性空间中从3G开始最大896M的区间,为直接内存映射区,该区域的线性...vmalloc分配的线性地址所对应的物理页可能处于低端内存,也可能处于高端内存。 (3
  • 进程代码中的地址为逻辑地址,经过段页式地址映射后,才真正访问物理内存。 段页式机制如下图。 Linux内核地址空间划分 通常32位Linux内核地址空间划分0~3G为用户空间,3~4G为内核空间。注意这里是3...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 16,551
精华内容 6,620
关键字:

高端内存映射