03.linux 内存管理

本文深入剖析内存管理的关键概念和技术,涵盖内核与用户空间通信、内存映射机制、内存分配策略等方面,重点介绍伙伴算法和slab算法的工作原理及其应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 内存寻址
     1.1 
     
     1.2

     1.3 内核页表
          1)概述:内核线性地址空间(32位:3G-4G线性地址空间)由内核页表维护,指明了内核线性地址空间如何映射到物理内存。每个进程的线性地址空间由mm_struct数据结构描述,mm_struct->pgd指针变量指向内核页表的主内核页全局目录。内核页表存储在swapper_pg_dir变量中,位置见下图。
          2)内核页表建立流程
               概述:
1. 内核映像刚刚被装入内存后,CPU运行于实模式,此时内核创建一个有限的地址空间(简单起见,假设为RAM的前8M)用于存储内核使用段(代码段,数据段等),临时页表(临时页全局目录是在内核编译过程中静态地初始化的,保存在swapper_pg_dir变量中)和128K动态数据结构存储区。并把物理地址从0x00000000-0x007fffff分别映射到线性地址空间的0x00000000-0x007fffff和0xc0000000-0x007fffff
2. 建立主内核页全局目录,最终映射必须把从0xc0000000开始的线性地址转换为从0开始的物理地址。其中主内核页全局目录保存在全局变量swapper_pg_dir(#define swapper_pg_dir ((pgd_t *) 0))中(然后,再删除临时主内核页全局目录)。             
               流程
                    1. 见3.linux kernel启动流程: 3.3 内核页表创建
              


2.关键数据结构
     1)struct page
          概述:页描述符,用来描述页框的状态信息(用来描述页框中存储的页的信息,即描述一个物理内存页)。
          备注:
                    #define virt_to_page(addr)宏,产生线性地址addr对应的页描述符地址
                    #define pfn_to_page(pfn)宏,产生与页框号pfn对应的页描述符地址
格式:
struct page {
struct address_space *mapping;
......
}
     2)struct page *mem_map
          概述:在系统启动时,初始化内存的所有页框,并创建对应的页描述符,然后将所有的页描述符存储在mem_map数组中。
     3)typedef struct pglist_data {
               }pg_data_t
          概述:物理内存节点描述符。(不同的平台访问不同内存节点的中的页面所需的时间不同,linux x86架构只设置一个内存节点)
                    pg_data_t *pgdat_list[MAX_NUMNODES]存放物理内存节点,在linux x86架构中,该数组中只有一个元素。           
     备注:
          关键宏定义
1)PHYS_OFFSET 
     概述:内存起始地址
     格式:
phys_addr_t memstart_addr __read_mostly = 0;
#define PHYS_OFFSET          ({ memstart_addr; })
                    2)PAGE_OFFSET
                         概述:内核线性地址空间的起始位置,32位为0xc0000000
                    3)PKMAP_BASE  
                         概述:永久内核映射的线性地址的起始位置
                         格式:        
#define PKMAP_BASE          (PAGE_OFFSET - PMD_SIZE)
(0xC0000000UL) - (1UL << 21)
BFE00000
                    4)FIXADDR_START
                         概述:固定映射的线性地址的起始位置
                    5)VMALLOC_START,VMALLOC_END
                         概述:非连续内存映射的线性地址的起始,结束位置



3.内存的物理地址空间与内存的线性地址空间

3.1 内存的物理地址
  3.1.1 X86平台,内存物理地址的管理区划分:
      a. 物理内存大于896M时
             1. 内存物理地址低端区(低端内存):0 ~ 896MB
                    1)0 ~ 16MB的物理页框,称作ZONE_DMA管理区
                    2)16MB ~ 896MB的物理页框,称作ZONE_NORMA管理区
             2. 内存物理地址高端去(高端内存):896MB ~ 
                    1)高于896MB的物理页框,称作ZONE_HIGHMEM管理区  
          备注:
               1)内存物理地址通常通过映射,成为线性地址,再被进程使用

  3.1.2 物理内存节点(node)
     1.概述
     不同的操作系统,其物理内存会被划分为几个节点(node),在一个单独的节点内,任一给定cpu访问页面所需的时间都是相同的。在每个节点内,都分3个管理区进行管理。
     在x86,arm平台,linux系统的物理内存被划分为只有1个节点(node =0),所有的内存都挂在这个节点0上。系统中定义了  pg_data_t *pgdat_list[MAX_NUMNODES]全局数组存放物理内存节点,此MAX_NUMNODES=0 ;

     2. 图解

               
          其中,ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM三个管理区的描述符为struct zone;
                    zone_mem_map是一个页框的数组,它记录了一个内存分区的所有页框的使用情况,zone_mem_map则指向第一个元素。
               
3.2 内存的线性地址
3.2.1 概述:
      1) 内存物理地址通常通过映射,成为线性地址,再被进程使用;
      2) 32位linux系统,内存的线性地址的空间大小为4G;
      3) 内存线性地址空间分为两个部分:1. 用户线性地址空间 (0X00000000 - 0XBFFFFFFF ,3GB);2. 内核线性地址空间(0XC0000000 - 0XFFFFFFFF ,1GB)。

3.2.2 内存物理地址与内存线性地址的映射
     a. 以内存物理地址的角度解析
1)低端内存(ZONE_DMA区 和ZONE_NORMAL区,0 ~ 896MB )线性的映射到内核线性地址空间(0XC0000000 - 0XF7FFFFFF )。进程访问这块内存线性地址,就相当于直接访问了内存物理地址。
2)高端内存(ZONE_HIGHMEM区 ,896MB ~ ),能够线性或非线性的映射到内存线性地址空间上(包括用户线性地址空间和内核线性地址空间)。虽然ZONE_HIGHMEM区也能线性的映射到内核线性地址空间,内核也不能直接通过访问这块线性地址来访问对应的物理地址。

     b. 以内存线性地址的角度解析
     1. 内核线性地址空间
1)内核线性地址0XC0000000 ~ 0XF7FFFFFF 与内存物理地址0~896MB(低端内存) 一一对应的线性映射
2)内核线性地址0XF8000000 ~ 0XFFFFFFFF 与内存物理地址896MB~(高端内存),可以线性映射,也可以非线性映射

     2.用户线性地址空间
          1)用户线性地址0x000000000XBFFFFFFF,可以映射到高端内存上(896MB~

3.2.3 内核线性地址空间映射
      x86平台:
                         
                         
                              备注:
                                   1)vmalloc区,用于非连续内存映射,也可称为动态内存映射区
                                   2)arm平台 PKMAP_BASE的地址
#define PKMAP_BASE          (PAGE_OFFSET - PMD_SIZE)
(0xC0000000UL) - (1UL << 21)
BFE00000
                                   3)16k内核段页表项,即主内核页全局目录

3.2.4 内存线性地址计算
a. 32位系统
1)
地址空间大小为2^32 = 4294967296 = 0x1 0000 0000 = 4G
地址空间范围为 0 ~ 0xFFFF FFFF =0 ~ 1111 1111 1111 1111 1111 1111 1111 1111
#define PAGE_SHIFT        12                                             
#define PAGE_SIZE        (_AC(1,UL) << PAGE_SHIFT)   //页大小,
#define PAGE_MASK        (~(PAGE_SIZE-1))

PAGE_SIZE = 2^PAGE_SHIFT = 2^12 = 4096 =0x0000 1000 =  0000 0000 0000 0000 0001 0000 0000 0000
PAGE_MASK = (~(4096-1)) = (~(0000 0000 0000 0000 0000 1111 1111 1111)) = 1111 1111 1111 1111 1111 0000 0000 0000 = 0xFFFF F000


#define PAGE_ALIGN(addr)    (((addr)+PAGE_SIZE-1)&PAGE_MASK) //进行页对象
eg:
    addr为0x22000001
    PAGE_ALIGN(addr)=(0x22000001+4096-1)&0xffff f000
    =(0x22000001+0xfff)&0xfffff000
    =0x22001000&0xfffff000
    =0x22001000;

          #define PAGE_CACHE_SHIFT    PAGE_SHIFT
          #define PAGE_CACHE_SIZE        PAGE_SIZE
          #define PAGE_CACHE_MASK        PAGE_MASK

2)addr & ~PAGE_MASK
     作用:
1. 可以判定addr是否是4096倍数,如果如果结果是1,则不是4096倍数
2. 可以获取在页内的偏移量

3.3 内核空间与用户空间通信
3.3.1 get_user()和put_user()
     概述:
1)在内核空间和用户空间交换数据时,get_user和put_user是两个两用的函数。相对于copy_to_user和copy_from_user(将在另一篇博客中分析),这两个函数主要用于完成一些简单类型变量(char、int、long等)的拷贝任务,对于一些复合类型的变量,比如数据结构或者数组类型,get_user和put_user函数还是无法胜任,这两个函数内部将对指针指向的对象长度进行检查,在arm平台上只支持长度为1,2,4,8的变量。

3.3.2 copy_to_user()和copy_from_user()

4.物理内存分配与释放

4.1 伙伴算法 - 页框分配
       a. 概述:
          1)使用伙伴算法分配内存中的页框,适用于内核态进程为申请者分配低端内存或高端内存中的页框
2)高端内存的页框分配,只能用alloc_pages()和alloc_page()分配。(这些函数不返回第一个被分配页框的线性地址,因为如果该页框属于高端内存,那么这样的线性地址根本不存在。取而代之,这些函数返回第一个被分配页框的页描述符的线性地址。)
          3) 伙伴算法,是以页框为单位分配释放内存区
       b. 请求分配
1)#define alloc_pages(gfp_mask, order)
概述:请求分配2的order次方个连续的页框,返回这片页框的第一个页框的页描述符地址(返回一个page数据结构的地址),失败则返回NULL。该方法通过伙伴算法获得连续的页框。
          参数: gfp_mask是一组标志位,可以通过它指定在那个管理区取得页框等。
                    (__GFP_DMA :所请求的页框必须处于ZONE_DMA管理区
                       __GFP_HIGHMEM :所请求的页框处于ZONE_HIGHMEM管理区)
          格式
#define alloc_pages(gfp_mask, order) \
        alloc_pages_node(numa_node_id(), gfp_mask, order)
->__alloc_pages(gfp_mask, order, node_zonelist(nid, gfp_mask));
          备注:伙伴算法,1.伙伴算法,将内存中所有空闲的页框分组为11个块链表,每个块链表中的表项分别包含1,2,4,8.....1024个连续页框
                                    2.用free_area数组表示这11个块链表,每个元素标识一种块大小,其中每个管理区都有自己的free_area数组
struct free_area {
     struct list_head     free_list[MIGRATE_TYPES]; 
     unsigned long          nr_free;
};
                                    3.分配块:static struct page *__rmqueue(struct zone *zone, unsigned int order,int migratetype)
         释放块: void __free_pages(struct page *page, unsigned int order)
2)#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
     概述:请求分配一个页框,返回所分配页框的页描述符地址,失败则返回NULL。
3)#define __get_free_pages(gfp_mask,order)
     概述:与alloc_pages类似,不同的是返回第一个页框的线性地址
     备注:
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
     struct page *page;
     //高端内存区(ZONE_HIGHMEM区)不能用该函数,只能用alloc_pages函数。因为高端内存区的页框可能还没有映射到线性地址上,所以该页框的就没有线性地址
     VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0);
     page = alloc_pages(gfp_mask, order);
     if (!page)
          return 0;
     return (unsigned long) page_address(page);
}
page_address(page) - > lowmem_page_address(page) - > __va(PFN_PHYS(page_to_pfn(page)))
其中,#define page_to_pfn(page)     ((unsigned long)((page) - mem_map) + PHYS_PFN_OFFSET) ,通过page的地址获取在mem_map数组的下标(或叫索引)然后加上一个偏移量得到页框的物理地址。因为是线性映射,所以该物理地址通过PFN_PHYS做一次偏移,得到对应的线性地址。
4)#define __get_free_page(gfp_mask)   __get_free_pages((gfp_mask), 0)
     概述:返回所分配页框的线性地址
5)#define __get_dma_pages(gfp_mask, order)  __get_free_pages((gfp_mask) | GFP_DMA, (order))
     概述:在ZONE_DMA管理区中请求分配2的order次方个连续的页框,返回第一个页框的线性地址
6)unsigned long get_zeroed_page(gfp_t gfp_mask)
{
     return __get_free_pages(gfp_mask | __GFP_ZERO, 0);
}
     概述:用来获取填满0的页框,然后返回所获取页框的线性地址

      c. 释放:
1)void __free_pages(struct page *page, unsigned int order)
     概述:先检查page指向的页描述符(参数为页描述符的地址),如果该页框未被保留(PG_reserved标志为0),就把描述符的count字段减1。如果count值变为0,就假定从与page对应的页框开始的2的order次方个连续页框不再被使用。在这种情况下,该函数释放页框。
2)void free_pages(unsigned long addr, unsigned int order)
     概述:与__free_pages类似,不同的是addr为页框的线性地址。
3)#define __free_page(page) __free_pages((page), 0)
     概述:释放page所指页描述符对应的页框(参数为页描述符的地址)
4)#define free_page(addr) free_pages((addr), 0)
     概述:释放线性地址为add的页框

4.2 slab算法 - 以对象的大小为基础来分配内存的大小              
    4.2.1 概述:
1) 伙伴算法是以页框为单位进行内存区分配与释放,而slab算法是以对象为单位进行内存区的分配与释放。所以,slab算法可以在一个页框内分配与释放若干大小(小于一个页框)的内存区(为了降低内碎片,把内存区按几何分布,即内存区的大小取决于2的幂)。(另外,内核建立了13个按几何分布的空闲内存区链表,它们的大小从32b到128kb)
2)高速缓存 ,指的是使用slab算法进行内存管理的内存区域,称为。在内核中,用高速缓存描述符 kmem_cache标识一个高速缓存区。一个高速缓存区可以分配1个页框,或是多个页框。
1. 系统中所有的高速缓存描述符都挂载在struct list_head slab_caches链表上。其中,高速缓存区分为两种类型,通用和专用;
2. 通用高速缓存区有两种内存分配方式,一种适用于ISA DMA分配,另一种适用于常规分配;对应这两种分配形式,系统将通用高速缓存区分成两类,分别由两个全局数组struct kmem_cache *kmalloc_dma_caches[ ] 和struct kmem_cache *kmalloc_caches[ ] 存储。这两个数组中的每一个元素是一个高速缓存描述符,标识指定大小(2的幂次方)高速缓冲区。这两个数组,在系统初始化的时候通过调用kmem_cache_init()函数初始化
3)高速缓存,在内存中的位置,位于低端内存内,内核代码之后的一部分区域(这块内存的物理地址是线性的映射成线性地址,所以高速)

     4.2.2 关键数据结构
1) 高速缓存描述符 -  struct kmem_cache                 ·
struct kmem_cache {
     unsigned int object_size;/* The original size of the object */   //高速缓存区的大小
     unsigned int size;     /* The aligned/padded/added on size  */  //对齐,填充,增加的大小
     unsigned int align;     /* Alignment as calculated */
     unsigned long flags;     /* Active flags on the slab */
     const char *name;     /* Slab name for sysfs */
     int refcount;          /* Use counter */
     void (*ctor)(void *);     /* Called on object slot creation */
     struct list_head list;     /* List of all slab caches on the system */
};
2) slab描述符 - struct slab
     概述:slab描述符,分为外部slab描述符和内部slab描述符,他们的不同点就是slab描述符存放的位置不同;内部slab描述符,位于分配给slab的第一个页框的起始位置

3)对象描述符 

     4.2.3 创建自定义高速缓存区及slab对象分配
 1)创建自定义的高速缓存描述符对象
                                   static struct kmem_cache kmem_cache_boot = {
     .batchcount = 1,
     .limit = BOOT_CPUCACHE_ENTRIES,
     .shared = 1,
     .size = sizeof(struct kmem_cache),
     .name = "kmem_cache",
};
 2)为自定义的高速缓存描述符对象分配页框
                               概述:然后通过伙伴算法根据高速缓存符中(kmem_cache_boot )定义的内存大小为其分配若干个连续页框    
                               方法:
                                    分配 - static void *kmem_getpages(struct kmem_cache *cachep, gfp_t flags, int nodeid)
-> alloc_pages_exact_node()
     ->__alloc_pages()//通过伙伴算法分配页框
                                    释放 - static void kmem_freepages(struct kmem_cache *cachep, void *addr)
 3) 为自定义的高速缓存描述符对象分配slab
                               方法:
                                    分配 - static int cache_grow(struct kmem_cache *cachep, gfp_t flags, int nodeid, void *objp) -> alloc_slabmgmt()
                                    释放 - static void slab_destroy(struct kmem_cache *cachep, struct slab *slabp)
 4)分配slab对象
                               方法:
                                    分配 - void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t)     //从指定的高速缓存区中(cachep)获取slab对象
void *kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags)
{
     void *ret = slab_alloc(s, gfpflags, _RET_IP_);
     trace_kmem_cache_alloc(_RET_IP_, ret, s->object_size, s->size, gfpflags);
     return ret;
}
                                    释放 - void kmem_cache_free(struct kmem_cache *cachep, void *objp)

     4.2.4 创建通用高速缓存区
1) 概述:在系统初始化的时候,通过kmem_cache_init()函数创建一个高速缓存区kmem_cache_boot ,然后通过create_kmalloc_cache()函数在kmem_cache_boot高速缓存区中创建自己的高速缓存区对象,并分配指定大小的页框
2)格式:
     kmem_cache_init()-> 
          static struct kmem_cache kmem_cache_boot = {
     .batchcount = 1,
     .limit = BOOT_CPUCACHE_ENTRIES,
     .shared = 1,
     .size = sizeof(struct kmem_cache),     //对齐,填充,增加的大小
     .name = "kmem_cache",
};
     slab.h ->
struct kmem_cache *kmem_cache;
kmem_cache = &kmem_cache_boot;
           方法1:
 struct kmem_cache *__init create_kmalloc_cache(const char *name, size_t size, unsigned long flags)
{
     struct kmem_cache *s = kmem_cache_zalloc(kmem_cache, GFP_NOWAIT); //在高速缓存区kmem_cache中,创建自己的高速缓存描述符对象(分配一个slab对象),调用kmem_cache_alloc(struct kmem_cache *s, gfp_t gfpflags)创建slab对象:kmem_cache
     if (!s)
          panic("Out of memory when creating slab %s\n", name);
     create_boot_cache(s, name, size, flags);     //初始化高速缓存描述符对象,并分配size大小的内存空间(根据size的大小,分配对应数量的页框)
     list_add(&s->list, &slab_caches);     //将高速缓存描述符插入全局链表slab_caches上
     s->refcount = 1;
     return s;
}
                                        方法2:
                                               struct kmem_cache *  kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *))
                                                  概述:
                                                  1)在"kmem_cache"高速缓冲区中分配一个高速缓存描述符对象(slab对象),高速缓存描述符对象的名称是name,该对象的高速缓存区大小为size
      4.2.5 通用slab对象分配
             1)概述:根据需要分配的内存大小,在全局数组kmalloc_caches中选取对应大小的高速缓存描述符,然后分配对象。
             2)
                  方法1. kmalloc()
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
     struct kmem_cache *cachep;
     void *ret;
     if (__builtin_constant_p(size)) {
          int i;
          if (!size)
               return ZERO_SIZE_PTR;
          if (WARN_ON_ONCE(size > KMALLOC_MAX_SIZE))
               return NULL;
          i = kmalloc_index(size);
#ifdef CONFIG_ZONE_DMA
          if (flags & GFP_DMA)
               cachep = kmalloc_dma_caches[i];
          else
#endif
               cachep = kmalloc_caches[i];
          ret = kmem_cache_alloc_trace(cachep, flags, size);  // ->ret = slab_alloc(cachep, flags, _RET_IP_);
                                                                                      // ->trace_kmalloc(_RET_IP_, ret,size, cachep->size, flags);
          return ret;
     }
     return __kmalloc(size, flags);
}
  方法2. kzalloc():
static inline void *kzalloc(size_t size, gfp_t flags)
{
    return kmalloc(size, flags | __GFP_ZERO);
}
备注:在用kmalloc()申请内存的时候,通常要用 memset() 来把申请的内存中的值初始化为0。而kzalloc()则是结合了上面两个步骤。
                 3)释放:     void kfree(const void *objp)                         

     4.2.6 高速缓存区结构图
                    x86平台:
               

               arm平台:
                    x86平台的kmem_list3 在arm平台为kmem_cache_node
               

4.3 其他内存分配
       4.3.1 原子内存分配
1)概述:原子请求从不被阻塞,如果没有足够的空闲页,则仅仅是分配失败而已。内核为原子内存分配请求保留了一个页框池,只有在内存不足时才使用,用来减少内存不足的情况发生。其中页框池,由ZONE_DMA管理区和ZONE_NORMA管理区内各自保留的一部分页框组成,各区分配给页框池的数目与这两个区的页框总数比例有关。
       4.3.2 每CPU页框高速缓存
           1) 概述:每个管理区为每个CPU分配了一个页框高速缓存,用于满足本地CPU发出的单一内存请求。
           2) 每个CPU页框高速缓存描述符:struct per_cpu_pageset , 在管理区描述符 struct zone ->pageset 字段中定义







6.物理内存映射
6.1 低端内存区(ZONE_DMA区和ZONE_NORMA区)内核映射
           1)概述:低端内存区是线性的映射到内核线性地址空间上(32位系统的物理内存大于896M的低端内存,有896M的物理地址线性的映射到3G-4G之间的内核线性地址空间,剩下的128M内核线性地址空间用于高端内存映射。低端内存物理地址=线性地址-PAGE_OFFSET),内核能够通过这片虚拟地址空间直接访问物理内存。
           2)低端内存区物理地址与其线性地址的关系
               #define __phys_to_virt(x)     ((unsigned long)((x) - PHYS_OFFSET + PAGE_OFFSET))     //物理地址+PAGE_OFFSET = 线性地址

6.2 高端内存区(ZONE_HIGHMEM)内核映射
          内核线程使用域(内存都是由内核分配的,内核就是进程的内核态)
           6.2.1 概述:内核线性地址空间的剩下128M线性地址空间用于高端内存映射(x86平台)。高端内存映射有3中机制(x86平台):永久内核映射,临时内核映射,非连续内存分配。
           6.2.2 高端内存的永久内核映射
                              1)概述:
                                        1. 建立高端页框到内核线性地址空间的长期映射称为永久内核映射。永久内核映射可能阻塞当前进程,所以不能再中断处理函数和可延长函数中调用。
                                        2. 在主内核页表中,有一个专门的页表(pkmap页表)用于标识高端内存区中高端页框的永久内核映射(即如果有一个高端页框建立了永久内核映射,则在pkmap页表中就有一个页表项指向该高端页框)。
                                                  1)pkmap页表的地址存储在pkmap_page_table变量中。
                                                  2)pkmap页表的页表项个数由LAST_PKMAP宏产生(取决于PAE是否被激活,激活则LAST_PKMAP = 512,不激活则LAST_PKMAP = 1024。所以内核一次最多访问2MB或4MB经过永久内核映射的高端内存页框,其中一个页框4kb)
                                                  3)pkmap页表的每个页表项都有一个计数器,所有页表项的计数器存储在pkmap_count数组中。
                                                            计数器=0,对应的页表项没有映射任何高端页框,且可用;
                                                            计数器=1,对应的页表项没有映射任何高端页框,但是它不能使用,因为相应的TLB表没有刷新
                                                            计数器=n(n>1),相应的页表项映射一个高端页框,有n-1个内核成分在使用这个页框
                                                  4)pkmap页表映射的线性地址从PKMAP_BASE 开始
                                                            arm平台:
#define PKMAP_BASE (PAGE_OFFSET - PMD_SIZE)
#define PAGE_OFFSET 0xC0000000
(0xC0000000UL) - (1UL << 21) = 0xBFE00000
                                        3. 内核使用一个page_address_htable散列表,记录高端页框与其永久内核映射的内核线性地址直接的关系。该散列表包含的数据结构为page_address_map,该数据结构包含一个指向页描述符的指针和分配给该页框的线性地址。

                              2)建立永久内核映射
                                   方法:void *kmap(struct page *page)
{
     might_sleep();
     if (!PageHighMem(page))                    //如果不是高端内存页框,则直接通过线性映射规则获取线性地址
          return page_address(page);
     return kmap_high(page);
}

void *kmap_high(struct page *page)
{
     unsigned long vaddr;
     lock_kmap();
     vaddr = (unsigned long)page_address(page);          //检查页框是否已经被映射
     if (!vaddr)
          vaddr = map_new_virtual(page);
     pkmap_count[PKMAP_NR(vaddr)]++;
     BUG_ON(pkmap_count[PKMAP_NR(vaddr)] < 2);
     unlock_kmap();
     return (void*) vaddr;
}

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])                    //1.从pkmap页表中获取一个的页表项,该页表项没有映射任何高端内存页框(计数器=0)
               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);          //5.如果没有找到合适的的页表项,则阻塞当前进程
               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 = PKMAP_ADDR(last_pkmap_nr);          //2.获取线性地址,#define PKMAP_ADDR(nr)          (PKMAP_BASE + ((nr) << PAGE_SHIFT))
     set_pte_at(&init_mm, vaddr,
             &(pkmap_page_table[last_pkmap_nr]), mk_pte(page, kmap_prot));     //3.将此高端内存页框插入pkmap页表
     pkmap_count[last_pkmap_nr] = 1;
     set_page_address(page, (void *)vaddr);     //4.将高端内存页框和其对应的线性地址,插入page_address_htable散列表
     return vaddr;
}
                              3)释放永久内核映射 :void kunmap_high(struct page *page)

                     6.2.2 高端内存的临时内核映射
                              1)概述:每个CPU都保留了13个固定映射的线性地址,用于临时内核映射。(在x86平台,这13个地址由enum km_type数据结构指定,该数据结构的每个元素(除了最后一个元素)能指定enum fixed_addresses {}数据结构中的一个索引号;在arm平台,则没有这个数据结构,cpu自行指定)
                                             临时内核映射不阻塞当前进程,可以使用在中断处理函数和可延长函数中
                              2)建立临时内核映射
                                   x86平台:
                                        void *kmap_atomic(struct page *page,enum km_type type)
                                   arm平台:
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);
          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(FIX_KMAP_BEGIN + idx);
     set_top_pte(vaddr, mk_pte(page, kmap_prot));
     return (void *)vaddr;
}
                              3)撤销临时内核映射
                                   arm平台:
void kunmap(struct page *page)
{
     BUG_ON(in_interrupt());
     if (!PageHighMem(page))
          return;
     kunmap_high(page);
}               
                              4)固定映射的线性地址
1. 概述:固定映射的线性地址,指的是这些线性地址为常量地址。其中常量地址的线性地址空间范围,在内核线性地址空间的剩余的128M线性地址空间里(x86平台)。并且这些常量地址可以以任意方式与物理内存页框建立映射关系。
2. 获取固定映射的线性地址
               方法:static inline unsigned long fix_to_virt(const unsigned int idx) //通过fixed_addresses 数据结构中的索引号idx(如:FIX_BTMAP_END ),计算出固定映射的线性地址
{
     if (idx >= FIX_KMAP_END)
          __this_fixmap_does_not_exist();
     return __fix_to_virt(idx);
}
                         #define __fix_to_virt(x)     (FIXADDR_START + ((x) << PAGE_SHIFT))
                         #define FIXADDR_START          0xfff00000UL     //固定映射线性地址的起始地址
                         #define PAGE_SHIFT          12          //页表项能够映射的区域大小的对数
               解析:
arm64 :
enum fixed_addresses {
     FIX_EARLYCON_MEM_BASE,
     __end_of_permanent_fixed_addresses,

#define NR_FIX_BTMAPS          4
#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,
     __end_of_fixed_addresses
};
 3. 物理页框映射到固定映射的线性地址
            映射方法:
                    x86平台:
                         set_fixmap(idx,phys)宏
                         set_fixmap_nocache(idx,phys)宏   //把页表项的PCD标志置位

                    arm平台:

                      撤销映射方法:
                              x86平台:
                                   clear_fixmap(idx)宏
                     6.2.3 高端内存的非连续内存映射
                         1)概述:非连续内存映射,指的是非连续的物理页框映射到连续的线性地址上。
                         2)非连续内存区描述符  - 用于描述一块vmalloc区 
struct vm_struct {
     struct vm_struct     *next;
     void               *addr;
     unsigned long          size;
     unsigned long          flags;
     struct page          **pages;  //指向物理页框数组的首地址
     unsigned int          nr_pages; //内存区填充的页的歌声
     phys_addr_t          phys_addr;
     const void          *caller;
};

                         3)非连续内存区 分配
                                   void *vmalloc(unsigned long size)
                                   void *vmalloc_32(unsigned long size)  在ZONE_NORMAL和ZONE_DMA内存管理区中分配页框

                                   分配流程:
1.先在vmalloc区获取一块连续的空闲线性地址
2.通过伙伴算法获取若干个单页框
3.页框映射到上述获取到的线性地址上                                  

                              非连续内存区 释放
                                   void vfree(const void *addr)
{
     kfree(addr);
}



7. 匿名共享内存

/dev/ashmem

int fd = -1;
size_t size;
fd = ashmem_create_region("gralloc-buffer", size);
private_handle_t* hnd = new private_handle_t(fd, size, 0);


#define ASHMEM_DEVICE    "/dev/ashmem"
system/core/libcutils/ashmem-dev.c
int ashmem_create_region(const char *name, size_t size)
{
    int fd, ret;
    fd = open(ASHMEM_DEVICE, O_RDWR);
    if (name) {
        char buf[ASHMEM_NAME_LEN];
        strlcpy(buf, name, sizeof(buf));
        ret = ioctl(fd, ASHMEM_SET_NAME, buf);
    }
    ret = ioctl(fd, ASHMEM_SET_SIZE, size);
    return fd;
}



system/core/libcutils/ashmem-host.c
int ashmem_create_region(const char *ignored, size_t size)
{

}




MemoryHeapBase::MemoryHeapBase(size_t size, uint32_t flags, char const * name)
    : mFD(-1), mSize(0), mBase(MAP_FAILED), mFlags(flags),
      mDevice(0), mNeedUnmap(false), mOffset(0)
{
    const size_t pagesize = getpagesize();
    size = ((size + pagesize-1) & ~(pagesize-1));
    int fd = ashmem_create_region(name == NULL ? "MemoryHeapBase" : name, size);
    ALOGE_IF(fd<0, "error creating ashmem region: %s", strerror(errno));
    if (fd >= 0) {
        if (mapfd(fd, size) == NO_ERROR) {
            if (flags & READ_ONLY) {
                ashmem_set_prot_region(fd, PROT_READ);
            }
        }
    }
}






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值