返回首页
当前位置: 主页 > 操作系统 > Linux教程 >

Linux内存管理基础教程

时间:2016-10-05 21:26来源:电脑教程学习网 www.etwiki.cn 编辑:admin

关于Linux的内存管理,本文分别从内核空间和用户空间两个视角来阐述

一、内核空间

1.1 页

页(page)是内核的内存管理基本单位。

==> linux/mm_types.h

struct page {
       page_flags_t flags;  页标志符
       atomic_t _count;    页引用计数
       atomic_t _mapcount;     页映射计数
       unsigned long private;    私有数据指针
       struct address_space *mapping;    该页所在地址空间描述结构指针,用于内容为文件的页帧
       pgoff_t index;               该页描述结构在地址空间radix树page_tree中的对象索引号即页号
       struct list_head lru;        最近最久未使用struct slab结构指针链表头变量
       void *virtual;               页虚拟地址
};
  • flags:页标志包含是不是脏的,是否被锁定等等,每一位单独表示一种状态,可同时表示出32种不同状态,定义在<linux/page-flags.h>
  • _count:计数值为-1表示未被使用。
  • virtual:页在虚拟内存中的地址,对于不能永久映射到内核空间的内存(比如高端内存),该值为NULL;需要事必须动态映射这些内存。

尽管处理器的最小可寻址单位通常为字或字节,但内存管理单元(MMU,把虚拟地址转换为物理地址的硬件设备)通常以页为单位处理。内核用struct page结构体表示每个物理页,struct page结构体占40个字节,假定系统物理页大小为4KB,对于4GB物理内存,1M个页面,故所有的页面page结构体共占有内存大小为40MB,相对系统4G,这个代价并不高。

1.2 区

内核把页划分在不同的区(zone)

总共3个区,具体如下:

描述 物理内存(MB)
ZONE_DMA DMA使用的页 <16
ZONE_NORMAL 可正常寻址的页 16 ~896
ZONE_HIGHMEM 动态映射的页 >896
  • 执行DMA操作的内存必须从ZONE_DMA区分配
  • 一般内存,既可从ZONE_DMA,也可从ZONE_NORMAL分配,但不能同时从两个区分配;

1.3 页分配与释放

下面列举所有的页为单位进行连续物理内存分配,也称为低级页分配器:

页分配函数 描述
alloc_pages(gfp_mask, order) 分配2^order个页,返回指向第一页的指针
alloc_pages(gfp_mask) 分配一页,返回指向页的指针
__get_free_pages(gfp_mask, order) 分配2^order个页,返回指向其逻辑地址的指针
__get_free_pages(gfp_mask) 分配一页,返回指向其逻辑地址的指针
get_zeroed_page(gfp_mask) 分配一页,并填充内容为0,返回指向其逻辑地址的指针
  • get_zeroed_page:对于用户空间,这个方法能保障系统敏感数据不会泄露
  • page_address: 把给定的页转换成逻辑地址
页释放函数 描述
__free_pages(page, order) 从page开始,释放2^order个页
free_pages(addr, order) 从地址addr开始,释放2^order个页
free_page(addr) 释放addr所在的那一页

1.4 字节分配与释放

kmalloc,vmalloc分配都是以字节为单位

(1) kmalloc

void * kmalloc(size_t size, gfp_t flags)

该函数返回的是一个指向内存块的指针,其内存块大小至少为size,所分配的内存在物理内存中连续且保持原有的数据(不清零)

其中部分flags取值说明:

  • GFP_USER: 用于用户空间的分配内存,可能休眠;
  • GFP_KERNEL:用于内核空间的内存分配,可能休眠;
  • GFP_ATOMIC:用于原子性的内存分配,不会休眠;典型原子性场景有中断处理程序,软中断,tasklet等

kmalloc内存分配最终总是调用__get_free_pages 来进行实际的分配,故前缀都是GFP_开头。 kmalloc分最多只能分配32个page大小的内存,每个page=4k,也就是128K大小,其中16个字节用来记录页描述结构。kmalloc分配的是常驻内存,不会被交换到文件中。最小分配单位是32或64字节。

kzalloc

kzalloc()等价于先用 kmalloc() 申请空间, 再用memset()来初始化,所有申请的元素都被初始化为0。

static inline void *kzalloc(size_t size, gfp_t flags)
{
    return kmalloc(size, flags | __GFP_ZERO); //通过或标志位__GFP_ZERO,初始化元素为0
}

(2) vmalloc

void * vmalloc(unsigned long size)

该函数返回的是一个指向内存块的指针,其内存块大小至少为size,所分配的内存是逻辑上连续的。

kmalloc不同,该函数乜有flags,默认是可以休眠的。

小结:

分配函数 区域 连续性 大小 释放函数 优势
kmalloc 内核空间 物理地址连续 最大值128K-16 kfree 性能更佳
vmalloc 内核空间 虚拟地址连续 更大 vfree 更易分配大内存
malloc 用户空间 虚拟地址连续 更大 free  

1.5 slab层

slab分配器的作用:

  • 对于频繁地分配和释放的数据结构,会缓存它;
  • 频繁分配和回收比如导致内存碎片,为了避免,空闲链表的缓存会连续的存放,已释放的数据结构又会放回空闲链表,不会导致碎片;
  • 让部分缓存专属单个处理器,分配和释放操作可以不加SMP锁;

slab层把不同的对象划分为高速缓存组,每个高速缓存组都存放不同类型的对象,每个对象类型对应一个高速缓存。kmalloc接口监理在slab层只是,使用一组通用高速缓存。

每个高速缓存都是用kmem_cache结构来表示

  • kmem_cache_crreate:创建高速缓存
  • kmem_cache_destroy: 撤销高速缓存
  • kmem_cache_alloc: 从高速缓存中返回一个指向对象的指针
  • kmem_cache_free:释放一个对象

实例分析: 内核初始化期间,/kernel/fork.c的fork_init()中会创建一个名叫task_struct的高速缓存; 每当进程调用fork()时,会通过dup_task_struct()创建一个新的进程描述符,并调用do_fork(),完成从高速缓存中获取对象。

1.6 栈的静态分配

当设置单页内核栈,那么每个进程的内核栈只有一页大小,这取决于编译时配置选项。 好处:

  • 可以减少每个进程内存的消耗;
  • 随着机器运行时间的增加,寻找两个未分配的、连续的页越来越困难,物理内存碎片化不断加重,那么给每个新进程分配虚拟内存的压力也增大;
  • 每个进程的调用链在自己的内核栈中,当单页栈选项被激活时,中断处理程序可获得自己的栈;

任意函数必须尽量节省栈资源, 方法就是所有函数让局部变量所占空间之和不要超过几百字节。

1.7 高端内存的映射

高端内存中的页不能永久地映射到内核地址空间。

  • kmap:把给定page结构映射到内核地址空间;
    • 当page位于低端内存,函数返回该页的虚拟地址
    • 当page位于高端内存,建立一个永久映射,再返回地址
  • kunmap: 永久映射的数量有限,应通过kunmap及时解除映射
  • kmap_atomic: 临时映射
  • kunmap_atomic: 解除临时映射

1.8 每个CPU数据

  • alloc_percpu: 给系统的每个处理器分配一个指定类型对象的实例,以单字节对齐;
  • free_percpu: 释放每个处理器的对象实例;
  • get_cpu_var: 返回一个执行当前处理器数据的特殊实例,同时会禁止内核抢占
  • put_cpu_var: 会重新激活内核抢占

使用每个CPU数据好处:

  • 减少了数据锁定,每个CPU访问自己CPU数据
  • 大大减少缓存失效,失效往往发生在一个处理器操作某个数据,而其他处理器缓存了该数据,那么必须清理或刷新缓存。持续不断的缓存失效称为缓存抖动。

1.9 小结

分配函数选择:

  1. 连续的物理页,使用低级页分配器 或kmalloc();
  2. 高端内存分配,使用alloc_pages(),返回page结构指针; 想获取地址指针,应使用kmap(),把高端内存映射到内核的逻辑地址空间;
  3. 仅仅需要虚拟地址连续页,使用vmalloc(),性能有所损失;
  4. 频繁创建和撤销大量数据结构,考虑建立slab高速缓存。

二、用户空间

用户空间中进程的内存,往往称为进程地址空间。Linux采用虚拟内存技术

2.1 地址空间

每个进程都有一个32位或64位的地址空间,取决于体系结构。 一个进程的地址空间与另一个进程的地址空间即使有相同的内存地址,也彼此互不相干,对于这种共享地址空间的进程称之为线程。一个进程可寻址4GB的虚拟内存(32位地址空间中),但不是所有虚拟地址都有权访问。对于进程可访问的地址空间称为内存区域。每个内存区域都具有对相关进程的可读、可写、可执行属性等相关权限设置。

内存区域可包含的对象:

  • 代码段(text section): 可执行文件代码
  • 数据段(data section): 可执行文件的已初始化全局变量(静态分配的变量和全局变量)。
  • bss段:程序中未初始化的全局变量,零页映射(页面的信息全部为0值)。
  • 进程用户空间栈的零页映射(进程的内核栈独立存在并由内核维护)
  • 每一个诸如C库或动态连接程序等共享库的代码段、数据段和bss也会被载入进程的地址空间
  • 任何内存映射文件
  • 任何共享内存段
  • 任何匿名的内存映射(比如由malloc()分配的内存)

这些内存区域不能相互覆盖,每一个进程都有不同的内存片段。

2.2 内存描述符

内存描述符由mm_struct结构体表示,

==> linux/sched.h

struct mm_struct
{
    struct vm_area_struct *mmap;
    rb_root_t mm_rb;
    ...
    atomic_t mm_users;
    atomic_t mm_count;

    struct list_head mmlist;
    ...
};
  • mm_users:代表正在使用该地址的进程数目,当该值为0时mm_count也变为0;
  • mm_count: 代表mm_struct的主引用计数,当该值为0说明没有任何指向该mm_struct结构体的引用,结构体会被撤销。
  • mmap和mm_rb:描述的对象都是相同的
    • mmap以链表形式存放, 利于高效地遍历所有元素
    • mm_rb以红黑树形式存放,适合搜索指定元素
  • mmlist:所有的mm_struct结构体都通过mmlist连接在一个双向链表中,该链表的首元素是init_mm内存描述符,它代表init进程的地址空间。

在进程的进程描述符(<linux/sched.h>中定义的task_struct结构体)中,mm域记录该进程使用的内存描述符。故current->mm代表当前进程的内存描述符。

fork()函数 利用copy_mm函数复制父进程的内存描述符,子进程中的mm_struct结构体通过allcote_mm()从高速缓存中分配得到。通常,每个进程都有唯一的mm_struct结构体,即唯一的进程地址空间。

当子进程与父进程是共享地址空间,可调用clone(),那么不再调用allcote_mm(),而是仅仅是将mm域指向父进程的mm,即 tsk->mm = current->mm。

相反地,撤销内存是exit_mm()函数,该函数会进行常规的撤销工作,更新一些统计量。

内核线程

  • 没有进程地址空间,即内核线程对应的进程描述符中mm=NULL
  • 内核线程直接使用前一个进程的内存描述符,仅仅使用地址空间中和内核内存相关的信息
顶一下
(1)
100%
踩一下
(0)
0%
标签(Tag):linux内存管理
------分隔线----------------------------
------分隔线----------------------------
发表评论
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
评价:
表情:
验证码:点击我更换图片
推荐内容