内存管理
博客专区 > China_OS 的博客 > 博客详情
内存管理
China_OS 发表于5年前
内存管理
  • 发表于 5年前
  • 阅读 235
  • 收藏 1
  • 点赞 1
  • 评论 0

【腾讯云】买域名送云解析+SSL证书+建站!>>>   

在内核中分配内存要比在用户空间分配内存复杂的多,接下来学习在内核中如何分配物理内存。


    内核把物理页作为管理内存的基本单位,尽管处理器可以处理的最小单位为字,但是内存管理单元(MMU,管理内存并把虚拟地址转化为物理地址)通常以页为单位进行处理,从虚拟内存角度来看,页就是最小单位。体系结构不同,支持页大小也不同,大多数32位机器支持4KB的页,64位机器支持8KB的页。

    内核中用struct_page()结构体来表示系统中的每个物理页:

struct page {
         unsigned long flags;                                                      
         atomic_t _count;                
         atomic_t _mapcount;          
         unsigned long private;          
         struct address_space *mapping;  
         pgoff_t index;                  
         struct list_head lru;   
         void *virtual;                  
};

    flag存放页状态,flag的每一位可以表示一种状态,所以它至少可以表示32种不同的状态。count存放页的引用计数,这个页被引用了多少次,-1表示该页在内核中没有被引用,可以分配他。内核使用page_count函数分配页。virtual是页的虚拟地址,通常情况下,他就是页在虚拟内存中的地址。

    页面结构和物理页相关,并非与虚拟页相关。内核仅仅使用该数据结构描述在当前物理页中存放的东西,这种数据结构的目的在于描述物理内存本身,而不是描述包含在其中的数据,系统中的每个物理页就要分配这样一个结构体。


    由于硬件限制,内核并不是对所有的页一视同仁,有些页位于内存中特定的物理位置上,由于存在这种限制,所以内核把页划分为不同的区,内核使用具有相似特性的页进行分组。由于硬件缺陷容易引起一下两种问题:
        1 一些硬件只能用某些特定的内存地址来执行DMA
        2 一些体系结构的内存的物理地址寻址范围比虚拟地址寻址范围大很多,这样一来就有一些内存不能永久的映射到内核空间。
       所以,linux使用了四种区:
              ZONE_DMA:这个区的页能用来执行DMA操作
              ZONE_DMA32:和ZONE_DMA类似,不过只能被32位设备访问
              ZONE_NORMAL:这个区包含的都是能正常映射的页
              ZONE_HIFHEM:这个区包含高端内存
    区的实际使用和分布体系结构相关。linux把页划分为区,形成不同的内存池,这样就可以根据用途进行分配了。区的划分没有任何意义,只不过是内核为了管理页而采取的一种逻辑上的分组。某些分配需要从特定的区中获取,而某些分配却可以从多个区中获取页。不是所有的体系结构都定义了全部区,内核中使用struct_zone()表示区。

获得页
    内核提供了一种请求内存的底层机制,提供了对他进行访问的几个接口,所有这些结构都以页为单位进行分配。如下:

           1  struct page * alloc_page(gfp_t gfp_mask,unsingned int order)该函数分配2^order个连续的物理页面,便返回一个指向第一页的结构体指针。
           2  void * page_address(struct page *page)返回一个给定物理页所在的逻辑地址的指针
           3  unsigned long _get_free_page(gfp_t  gfp_mask,unsinged int_order)这个函数域alloc_pages作用相同,不过他返回的是第一个页的逻辑地址。
           4  unsigned long get_zeroed_page(unsigned int gfp_mask) 该函数作用同_get_free_pages一样,不过他把返回的页都填充成0。
           5  void free_pages(unsigned long addr)释放页,由于内核是完全信赖自己的,所以如果参数传入错误可能会导致系统崩溃。

    可以使用以下函数释放他们:


kmalloc()
    kmalloc函数与用户空间的malloc函数类似,只不过他可以获得以字节为单位的一块内核内存。这个函数返回一个指向内存块的指针,所分配的内存在物理上是连续的,如果内存足够可用,内核一般都会分配成功。

kfree()
    kmalloc的另一端就是kfree,他主要是用来释放由kmalloc分配的内存块。如果要释放的内存不是由kmalloc分配的或者已经释放了,则调用kfree会产生严重后果。

vmalloc()
    vmalloc的工作方式类似于kmalloc,只不过前者分配的内存虚拟地址是连续的,而物理地址则无需连续,这也是用户空间分配函数的工作方式。由malloc返回的页在进程的虚拟地址空间是连续的,但并不保证在物理空间是连续的。一般情况下只有物理设备需要得到连续的物理内存,为了性能上的考虑内核使用kmalloc分配内存。

gfp_mask标志
    分配器标志可以分为三类:行为修饰符、区修饰符、类型。行为修饰符表示内核应当如何分配所需的内存。区修饰符表示从哪分配内存。类型标志组合了行为修饰符和区修饰符,将各种可能用到的组合归纳为不同的类型。

slab层
    分配和释放数据结构是内核中最普遍的操作之一,为了便于快速分配和回收,coder一般喜欢创建一些空闲链表,空闲链表包含可以使用的,已经分配好的数据块。需要时只要在其中抓去一个即可,不需要时把他放进空闲链表而不是释放,这就相当于高速缓存。空闲链表的问题是不能全局控制,当内存紧缺时,内核无法让空闲链表释放内存。这时linux使用的slab层来扮演了数据结构缓存层的角色。slab试图在几个基本原则之间寻求一种平衡:
        1  频繁使用的数据结构也会频繁的分配和释放,因此应当缓存他们。
        2  频繁的分配和释放会导致内存碎片,为了避免这种情况,空闲链表会连续的存放。回收的对象可以立即投入下一次分配。
        3  如果分配器知道对象大小、页大小和总的高速缓存的大小,他会做出更明智的选择。
        4  如果让部分缓存专属于单个处理器,那么,分配和释放就可以在不加锁的情况下进行。
        5  对存放进行着色,防止多个对象映射到相同的高速缓存行。

slab层设计  
    slab把不同的对象划分为所谓的高速缓存组,每个高速缓存组存放不同类型的对象,每种对象类型对应一个高速缓存。slab由一个或者多个物理上连续的页组成,一般情况下由一页组成,每个高速缓存可以由多个slab组成,每个slab都包含一些对象成员,这里的对象成员是被缓存的数据结构。每个slab处于三种状态:满、部分满、空。每个高速缓存都使用kmem_cache结构体来表示。这个结构包含三个链表:slabs_full、slabs_partial、slabs_empty,均存放在kmem_list3结构内。这些链表包含高速缓存中的所有slab,slab描述符struct slab用来描述每个slab:

struct slab {
        struct list_head  list;       /*满,部分满或空链表*/
        unsigned long     colouroff;  /*slab着色的偏移量*/
        void              *s_mem;     /*在slab中的第一个对象*/
        unsigned int      inuse;      /*已分配的对象数*/
        kmem_bufctl_t     free;       /*第一个空闲对象*/
};

    slab描述符要么在slab之外进行分配,要么就在slab自身开始的地方。slab分配器可以创建新的slab,通过_get_free_pages低级内核页分配器进行,使用该函数来为高速缓存分配足够的内存。当在内存变得紧缺时,系统试图释放出更多的内存以供使用,或者当高速缓存被显示撤销时会调用kmem_freepages释放内存。

在栈上的静态分配
    用户空间能够承担起非常大的栈,而且栈空间还可以动态增长,而内核栈小而且固定。给每个进程分配一个固定大小的栈,可以减少内存消耗,内核也无需负担太重的栈管理任务。每个进程的内核栈大小依赖于体系结构,历史上每个进程都有两页大小的内核栈。

    在2.6内核中,引入了单页内核栈,当激活该选项时,每个进程的内核栈只有一页。这样的好处是:可以让每个进程减少内存消耗,而且随着机器运行时间的增加,分配两个连续的页越来越困难。总的来说内核栈可以是一页也可以是两页,这取决于编译时的配置。

高端内存的映射
    在高端内存中包含的页不能被永久的映射到内核的地址空间上,因此通过alloc_pages获取的页不可能有逻辑地址,在X86体系上,高于896M的物理内存的范围都是高端内存,他并不会永久的映射到内核地址空间,一旦这些页被分配就必须映射到内核的逻辑地址空间上。

    要映射一个给定的page到内核地址空间,可以使用kmap函数,这个函数在高端内存和低端内存上都可以使用,如果page对应的是低端内存中的页,函数只会单纯的返回该页的虚拟地址,如果page位于高端内存,则会建立一个永久映射,再返回地址,这个函数可以睡眠,因此只能用在进程上下文中,当不需要映射时使用kunmap函数解除映射。当必须映射一个页面而又不能睡眠时,内核提供了临时映射,有一组保留的临时映射,他们可以存放新建的临时映射。例如在中断处理中就会使用临时映射。

每个CPU分配
    支持SMP的现代操作系统使用每个cpu上的数据,对于每个给定的处理器其数据是唯一的。一般来说,每个cpu的数据存放在一个数组中。数组中的每一项对应着系统上一个存在的处理器。2.6内核为了方便创建和操作每个cpu数据,引进了新的操作接口,称作precpu,该接口简化了创建和操作每个淳朴的数据。使用每个cpu数据有不少好处,首先减少了数据锁定,因为按照每个处理器访问每个cpu数据的逻辑,你不要任何锁。这只是一个单纯的编程约定,系统本身并不存在任何措施禁止你从事欺骗活动。第二个好处是每个cpu数据可以大大减少缓存失效,持续不断的缓存失效称为缓存抖动,这对系统性能影响较大。综上,使用每个cpu数据会省去数据上锁,他唯一的要求就是要求禁止内核抢占,这个过程接口会自动帮你完成。每个cpu数据在进程上下文和进程上下文中都很安全,但是不要睡眠,否则,你醒来后可能就在其他cpu上了。



标签: linux 内存管理
  • 打赏
  • 点赞
  • 收藏
  • 分享
共有 人打赏支持
粉丝 397
博文 383
码字总数 483581
×
China_OS
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: