6.内存管理

内存管理

在内核中分配内存不像在其他地方分配内存那么容易。造成这种局面的因素很多,根本原因是内核本身不能像用户空间那样奢侈地使用内存。

1.页

内核把物理页作为内存管理的基本单位。内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址的硬件)通常以页为单位。体系结构不同,支持的页大小也不同。内核用struct page结构表示每个物理页:

struct page {
         unsigned long flags;                                                      
         atomic_t count;                
         unsigned int mapcount;          
         unsigned long private;          
         struct address_space *mapping;  
         pgoff_t index;                  
         struct list_head lru;  
         void *virtual;                  
};

对上面重要变量说明:

  • flag的每一位单独表示一个状态,标志定义在
  • count存放页的引用次数,为0则是空闲页
  • virtual是页的虚拟地址

2.区

由于硬件限制,内核对页不能一视同仁。有些页位于内存特定的物理地址上,不能用于一些特定的任务,因此内核把页划分为不同的区(zone)。Linux必须处理如下两种由于硬件缺陷而引起的内存寻址问题:

  • 一些硬件只能用某些特定内存来执行DMA(直接内存访问)
  • 一些体系结构的内存物理寻址范围比虚拟寻址范围大的多,因此部分内存永远无法映射到内核空间

因此Linux主要存在四种区:

  • ZONE_DMA,包含的页可以执行DMA
  • ZONE_DMA32,和ZONE_DMA不同在于,这些页面只能被32位设备访问,某些体系下该区比ZONE_DMA更大
  • ZONE_NORMAL,能够正常映射的页
  • ZONE_HIGHMEM,不能永久被映射到内核空间地址的区

每个区都用struct zone表示,定义在

struct zone {
         spinlock_t              lock;
         unsigned long           free_pages;
         unsigned long           pages_min, pages_low, pages_high;
         unsigned long           protection[MAX_NR_ZONES];
         spinlock_t              lru_lock;       
         struct list_head        active_list;
         struct list_head        inactive_list;
         unsigned long           nr_scan_active;
         unsigned long           nr_scan_inactive;
         unsigned long           nr_active;
         unsigned long           nr_inactive;
         int                     all_unreclaimable; 
         unsigned long           pages_scanned;    
         struct free_area        free_area[MAX_ORDER];
         wait_queue_head_t       * wait_table;
         unsigned long           wait_table_size;
         unsigned long           wait_table_bits;
         struct per_cpu_pageset  pageset[NR_CPUS];
         struct pglist_data      *zone_pgdat;
         struct page             *zone_mem_map;
         unsigned long           zone_start_pfn;
 
         char                    *name;
         unsigned long           spanned_pages;  
         unsigned long           present_pages;  
};

其中,lock是自旋锁防止该结构被并发访问;watermark数组持有该区的最小值、最低和最高水位值;name是以NULL结尾的区名字,三个区名字为DMANormalHighMem

3.获得页

前面了解了页和区的概念,下面讲述如何请求和释放页。

请求页

标志 描述
alloc_page(gfp_mask) 只分配一页,返回指向页结构的指针
alloc_pages(gfp_mask, order) 分配2^order页,返回指向第一页页结构的指针
__get_free_page(gfp_mask) 只分配一页,返回指向其逻辑地址的指针
__get_free_pages(gfp_mask, order) 分配2^order页,返回指向第一页逻辑地址的指针
get_zeroed_page(gfp_mask) 只分配一页,让其内容填充0,返回指向逻辑地址的指针

释放页

释放页需要谨慎,只能释放属于你的页。传递了错误的struct page或地址,,用了错误的order值都可能导致系统崩溃。

例如释放8个页:

free_pages(page, 3)

可以看到释放过程与C语言的释放内存很相似的。

4.kmalloc()

上述的方法是对以页为单位的连续物理页,而以字节为单位的分配,内核提供的函数是kmalloc()。使用方法和malloc()类似,只是多了一个flags参数,其在中声明:

void * kmalloc(size_t size, gfp_t flags)

kmalloc()对应的函数就是kfree()kfree()声明于中:

void kfree(const void *ptr)

5.vmalloc()

vmalloc()kmalloc()工作方式类似,但是kmalloc()使用的连续的物理地址。vmalloc()使用非连续的物理地址,该函数为了把物理上不连续的页转换为虚拟地址空间上连续的页,必须专门建立页表项。

大多数情况下,一般硬件设备需要使用连续的物理地址,而软件可以使用非连续的物理地址,但是大多数情况,为了性能提升,内核往往用kmalloc()更多。

vmalloc()函数声明在中,定义在中。用法和用户空间的malloc()相同:

void * vmalloc(unsigned long size)

释放通过vmalloc()所获得的内存,使用下面函数:

void vfree(const void *addr)

6.slab层

分配和释放数据结构是所有内核中最常用操作之一。为了便于数据的频繁分配和回收,编程人员常常会用到空闲链表空闲链表包含可供使用的、已经分配好的数据结构块。当代名需要一个新的数据结构实例时,就可以从空闲链表中抓取一个,而不需要分配内存,再把数据存放进去。不需要这个数据结构的实例时,就放回空闲链表,而不是释放它。空闲链表相对于对象的高速缓存——快速存储频繁使用的对象类型(这个策略简直是awesome!)。

没有免费的蛋糕,对于空闲链表存在的主要问题是无法全局控制。当内存紧缺时,内核无法通知每个空闲链表,让其收缩缓存的大小,以便释放部分内存。实际上,内核根本就不知道任何空闲链表。因此未来弥补这个缺陷,Linux内核提供了slab层(也就是所谓的slab分配器)。slab分配器扮演了通用数据结构缓存层的角色。对于slab分配器设计需要考虑一下几个原则:

  • 频繁使用的数据结构也会频繁分配和释放,因此应当缓存它们。
  • 频繁分配和回收必然会导致内存碎片。为了避免这种情况,空闲链表的缓存会连续地存放。因为已释放的数据结构又会放回空闲链表,不会导致碎片。
  • 回收的对象可以立即投入下一次分配,因此,对于频繁的分配和释放,空闲链表能够提高其性能。
  • 如果让部分缓存专属于单个处理器,那么,分配和释放就可以在不加SMP锁的情况下进行。
  • 对存放的对象进行着色,以防止多个对象映射到相同的高速缓存行。

slab层把不同的对象划分为所谓的高速缓存组,其中每个高速缓存都存放不同类型的对象,每种对象类型对应一个高速缓存,例如一个高速缓存用于task_struct,一个用于struct inode。kmalloc()接口建立在slab层上,使用了一组通用高速缓存。这些缓存又被分为slabs,slab由一个或多个物理上连续的页组成,一般情况下,slab也就仅仅由一页组成。每个高速缓存可以由多个slab组成。每个slab都包含一些对象成员,这里的对象指的是被缓存的数据结构,每个slab处于三种状态之一:满,部分满,空。当内核的某一部分需要一个新的对象时,先从部分满的slab中进行分配。如果没有部分满的slab,就从空的slab中进行分配。如果没有空的slab,就要创建一个slab了。下图给出高速缓存,slab及对象之间的关系:

6.内存管理_第1张图片
高速缓存、slab和对象关系

每个缓存都使用kmem_catche结构表示,结构中包含3个链表。这些链表包含高速缓存所有的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层负责内存紧缺情况下所有底层的对齐、着色、分配、释放和回收等。

7.栈上的静态分配

在前面讨论的分配例子,不少可以分配到栈上。用户空间可以奢侈地负担很大的栈,而且栈空间还可以动态增长,相反内核空间不能——栈小而固定。给每个进程分配一个固定小栈,可以减小内存消耗和栈管理任务负担。

进程的内核栈大小既依赖体系结构,也和编译时的选项有关。在任何一个函数中,都必须尽量节省栈资源。让函数所有局部变量之后不要超过几百字节(栈上分配大量的静态分配是不理智的),栈溢出就会覆盖掉临近堆栈末端的数据。首先就是前面讲的thread_info

8.每个CPU使用数据

支持SMP的操作系统使用每个CPU上的数据,对于给定的处理器其数据是唯一的。一般而言,每个CPU的数据存放在一个数组内,数组中的每一项对应着系统上一个存在的处理器,安装当前处理器号就能确定这个数组的当前元素。

在Linux中引入了新的操作接口称为percpu,头文件声明了所有接口操作例程,可以在文件mm/slab.c找到定义。

使用每个CPU数据的好处是:

  • 减少了数据锁定
  • 大大减少了缓存失效,一个CPU操作另一个CPU的数据时,必须清理另一个CPU的缓存并刷新,存在不断的缓存失效。持续不断的缓存失效称为缓存抖动

这种方式的唯一安全要求就是禁止内核抢占,同时注意进程在访问每个CPU数据过程中不能睡眠——否则,唤醒之后可能已经到其他处理器上了。

你可能感兴趣的:(6.内存管理)