Linux内存管理


1.Linux 进程在内存数据结构

     可以看到一个可执行程序在存储(没有调入内存)时分为代码段,数据,未初始化数据三部分:

     1) 代码段存放CPU执行的机器指令。通常代码区是共享的,即其它执行程序可调用它。假如机器中有数个进程运行相同的一个程序,那么它们就可以使用同一个代码段。
     2) 据段存放已初始化的全局变量,静态变量(包括全局和局部的),常量。static全局变量和static函数只能在当前文件中被调用。
      3) 未初始化数据区(uninitializeddata segment,BSS):存放全局未初始化的变量。BSS的数据在程序开始执行之前被初始化为0或NULL。

      代码区所在的地址空间最低,往上依次是数据区和BSS区,并且数据区和BSS区在内存中是紧挨着的。。

     可执行程序在运行时又多出了两个区域:栈段(Stack)和堆段(Heap)。
      4) 栈区:由编译器自动释放,存放函数的参数值,局部变量等。每当一个函数被调用时,该函数的返回类型和一些调用的信息被存储到栈中。然后这个被调用的函数再为它的自动变量和临时变量在栈上分配空间。每调用一个函数一个新的栈就会被使用。栈区是从高地址位向低地址位增长的,是一块连续的内在区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。
       5) 堆段:用于存放进程运行中被动态分配的内存段,位于BSS和栈中间的地址位。由程序员申请分配(malloc)和释放(free)。堆是从低地址位向高地址位增长,采用链式存储结构。频繁地malloc/free造成内存空间的不连续,产生碎片。当申请堆空间时库函数按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。

      这个5中内存区域中数据段、BSS和堆通常是被连续存储的——内存位置上是连续的,而代码段和栈往往会被独立存放。有趣的是堆和栈两个区域关系很“暧昧”,他们一个向下“长”(i386体系结构中栈向下、堆向上),一个向上“长”,相对而生。但你不必担心他们会碰头,因为他们之间间隔很大(到底大到多少,你可以从下面的例子程序计算一下),绝少有机会能碰到一起。

下图简要描述了进程内存区域的分布:

       


2.地址相干概念

1. 虚拟地址、物理地址、逻辑地址、线性地址

 虚拟地址又叫线性地址。linux没有采用分段机制,所以逻辑地址和虚拟地址(线性地址)(在用户态,内核态逻辑地址专指下文说的线性偏移前的地址)是一个概念。物理地址自不必提。内核的虚拟地址和物理地址,大部分只差一个线性偏移量。用户空间的虚拟地址和物理地址则采用了多级页表进行映射,但仍称之为线性地址。

2. DMA/HIGH_MEM/NROMAL 分区

在x86结构中,Linux内核虚拟地址空间划分0~3G为用户空间,3~4G为内核空间(注意,内核可以使用的线性地址只有1G)。内核虚拟空间(3G~4G)又划分为三种类型的区:
ZONE_DMA 3G之后起始的16MB
ZONE_NORMAL 16MB~896MB
ZONE_HIGHMEM 896MB ~1G

由于内核的虚拟和物理地址只差一个偏移量:物理地址 = 逻辑地址 – 0xC0000000。所以如果1G内核空间完全用来线性映射,显然物理内存也只能访问到1G区间,这显然是不合理的。HIGHMEM就是为了解决这个问题,专门开辟的一块不必线性映射,可以灵活定制映射,以便访问1G以上物理内存的区域。从网上扣来一图,

高端内存的划分,又如下图,

内核直接映射空间 PAGE_OFFSET~VMALLOC_START,kmalloc和__get_free_page()分配的是这里的页面。二者是借助slab分配器,直接分配物理页再转换为逻辑地址(物理地址连续)。适合分配小段内存。此区域 包含了内核镜像、物理页框表mem_map等资源。


内核动态映射空间 VMALLOC_START~VMALLOC_END,被vmalloc用到,可表示的空间大。

内核永久映射空间 PKMAP_BASE ~ FIXADDR_START,kmap

内核临时映射空间 FIXADDR_START~FIXADDR_TOP,kmap_atomic



3.相干函数

内核把物理页作为内存管理的基本单位;内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址)通常以页为单位进行处理。MMU以页大小为单位来管理系统中的页表。从虚拟内存的角度看,页就是最小单位。

32位系统:页大小4KB

64位系统:页大小8KB

在支持4KB页大小并有1GB物理内存的机器上,物理内存会被划分为262144个页。内核用 struct page 结构表示系统中的每个物理页。

struct page {

page_flags_t flags; /* 表示页的状态,每一位表示一种状态*/

atomic_t _count; /* 存放页的引用计数,0代表没有被引用 */

atomic_t _mapcount;

unsigned long private;

strcut address_space *mapping;

pgoff_t index;

struct list_head lru;

void *virtual; /* 页在虚拟内存中的地址,动态映射物理页 */

}

下面,我们来解释下其中的重要字段。

flags:这个字段用于存放页的状态。这些状态包括页是不是脏的,是不是被锁定在内存中等。 flag 的每一位单独表示一种状态,所以,它至少可以同时表示出32种不同的状态。

_count:这个字段存放页的使用计数,也就是这个页被引用了多少次。很奇怪,技术值变为 -1 时,就说明当前内核并没有引用这一页,于是,在新的分配中就可以使用它,注意,这个字段使用的是 -1 代表未使用,而不是 0 。

virtual:这个字段是页的虚拟地址。

mapping:这个域指向和这个页关联的address_space 对象。

private:这个根据名字就可以看得出,它指向私有数据。

内核通过这样的数据结构管理系统中所有的页,因为内核需要知道一个页是否空闲,谁有拥有这个页。拥有者可能是:用户空间进程、动态分配的内核数据、静态内核代码、页高速缓存等等。系统中每一个物理页都要分配这样一个结构体,进行内存管理。


由于硬件的限制,内核并不能对所有的页一视同仁。Linux必须处理如下两种由于硬件存在缺陷而引起的内存寻址问题:

1)一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问)。

2)一些体系结构其内存的物理寻址范围比虚拟寻址范围大得多。这样,就有一些内存不能永久地映射到内核空间上。

由于存在这种限制,内核把具有相似特性的页划分为不同的区(ZONE):

1)ZONE_DMA——这个区包含的页能用来执行DMA操作。

2)ZONE_NORMAL——这个区包含的都是能正常地映射网页。

3)ZONE_DMA32——同上,不过只能被32位设备访问

4)ZONE_HIGHMEM——这个区包含“高端内存”,其中的页并能不永久地映射到内核地址空间。

Linux把系统的页划分为区,形成不同的内存池,这样就可以根据用途进行分配。注意,区的划分没有任何物理意义,这只是内核为了管理页而采取的一种逻辑上的分组。用于DMA的内存必须从ZONE_DMA中进行分配,但是一般用途的内存却既能从ZONE_DMA分配,也能从ZONE_NORMAL分配。



内核提供了一种请求内存的底层机制,并提供了对它进行访问的几个接口。所有这些接口都以页为单位分配内存,定义于<linux/gfp.h>。最核心的函数是:

struct page *alloc_pages( unsigned int gfp_mask, unsigned int order );

该函数分配 2 order 个连续的物理页,并返回一个指向第一页的 page 结构体指针,如果出错就返回NULL。

void *page_address( struct page *page );

把给定的页转换成它的逻辑地址。如果无须用到 struct page,可以调用:

unsigned long __get_free_pages( unsigned int gfp_mask, unsigned int order );

这个函数与alloc_pages 作用相同,不过它直接返回所请求的第一个页的逻辑地址。因为页是连续的,因此其他页也会紧随其后。

如果只需要一页,可以用以下两个函数:

struct page *alloc_page( unsigned int gfp_mask );

unsigned long _get_free_page( unsigned int gfp_mask );

如果需要让返回页的内容全为0,可以使用下面这个函数

unsigned long get_zeroed_page(unsigned int gfp_mask );

方法

描述

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,返回指向其逻辑地址的指针

当不再需要页时可以使用以下函数来释放它。

void  __free_pages( struct page *page, unsigned int order );

void free_pages( unsigned long addr, unsigned int order );

void free_page( unsigned long addr );

释放页时要谨慎,只能释放属于你的页。传递了错误的 struct page 或地址,用了错误的 order 值都可能导致系统崩溃。请记住,内核是完全依赖自己的。



kmalloc 与 malloc 一族函数非常类似,只不过它多了一个 flags 参数。kmalloc在<linux/slab.h>中声明:

void*kmalloc( size_t size, int flags );

这个函数返回一个指向内存块的指针,其内存块至少要有 size 大小。所分配的内存正在物理上是连续的。在出错时,它返回 NULL。除非没有足够的内存可用,否则内核总能分配成功。在对 kmalloc 调用之后,你必须检查返回的是不是 NULL,如果是,要适当地处理错误。

在低级页分配函数还是 kmalloc 中,都用到了gfp_mask(分配器标志)。这些标志可分为三类:行为修饰符、区修饰符及类型。

1)行为修饰符表示内核应当如何分配所需的内存。在某些特定情况下,只能使用某些特定的方法分配内存。例如,中断处理程序就要求内核在分配内存的过程中不能睡眠(因为中断处理程序不能被重新调度)。

2)区修饰符指明到底从哪一区中进行分配。

3)类型标志组合了行为修饰符和区修饰符,将各种可能用到的组合归纳为不同类型,简化了修饰符的使用。

kmalloc 的另一端就是 kfree,kfree声明于<linux/slab.h>中

voidk free( constvoid *ptr );

kfree 函数释放由 kmalloc分配出来的内存块。调用 kfree( NULL ) 是安全的。


vmalloc 的工作方式是类似于 kmalloc,只不过前者分配的内存虚拟地址是连续的,而物理地址则无需连续。这也是用户空间分配函数的工作方式:由malloc()返回的页在进程的虚拟地址空间内是连续的,但是这并不保证他们在物理RAM中也是连续的。kmalloc()函数确保页在物理地址上是连续。vmalloc函数值确保在虚拟地址空间内是连续的。它通过分配非连续的物理内存块,在修订页表,把内存映射到逻辑地址空间的连续区域中,就能做到这点。

大多数情况下,只有硬件设备需要得到物理地址连续的内存,因为硬件设备存在内存管理单元以外,它根本不理解什么是虚拟地址。尽管仅仅在某些情况下才需要物理上连续的内存块,但是很多内核都有kmalloc()来获取内存,而不是vmalloc()。这主要出于性能方面的考虑。vmalloc()函数为了把物理上不连续的页转换成虚拟地址空间上连续的页,必须专门建立页表项。糟糕的是,通过vmalloc()获得的页必须一个一个地进行映射。因为这些原因,一般是在为了获得大块内存时,例如当模块被动态插入内核时,就把模块装载到由vmalloc()分配的内存上。

void *vmalloc(unsigned long size)

该函数返回一个指针,指向逻辑上连续的一块内存,其大小至少为size。在发生错误时,函数返回NULL。函数可能睡眠,因此么不能从中断上下文中进行调用,也不能从其他不允许阻塞的情况下进行调用。

释放通过vfree()函数

void vfree(const void *addr)


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值