精华内容
下载资源
问答
  • linux内存管理初始化

    千次阅读 2014-12-28 00:21:08
    而对其初始化是了解整个内存管理子系统的基础。对相关数据结构的初始化是从全局启动例程start_kernel开始的。本文详细描述了从bootloader跳转到linux内核内存管理子系统初始化期间所做的操作,从而来加深对内存管理...

    内存管理子系统是linux内核最核心最重要的一部分,内核的其他部分都需要在内存管理子系统的基础上运行。而对其初始化是了解整个内存管理子系统的基础。对相关数据结构的初始化是从全局启动例程start_kernel开始的。本文详细描述了从bootloader跳转到linux内核内存管理子系统初始化期间所做的操作,从而来加深对内存管理子系统知识的理解和掌握。

    内核的入口是stext,这是在arch/arm/kernel/vmlinux.lds.S中指定的。而符号stext是在arch/arm/kernel/head.S中定义的。整个初始化分为两个阶段,首先是在head.S中用汇编代码执行一些平台相关的初始化,完成后跳转到start_kernel函数用C语言代码执行剩余的通用初始化部分。整个初始化流程图如下图所示:


    一、     启动条件
    通常从系统上电到运行到linux kenel这部分的任务是由boot loader来完成。Boot loader在跳转到kernel之前要完成一些限制条件:

    1、CPU必须处于SVC(supervisor)模式,并且IRQFIQ中断必须是禁止的;

    2、MMU(内存管理单元)必须是关闭的此时虚拟地址对应物理地址;

    3、数据cache(Data cache)必须是关闭的;

    4、指令cache(Instruction cache)没有强制要求;

    5、CPU通用寄存器0(r0)必须是0;

    6、CPU通用寄存器1(r1)必须是ARM Linux machine type

    7、CPU通用寄存器2(r2)必须是kernel parameter list的物理地址;

    二、     汇编代码初始化部分

    汇编代码部分主要完成的工作如下所示,下文只是概述head.S中各个函数完成的主要

    功能,具体的技术细节,本文不再赘述。

    ?  确定processor type

    ?  确定machine type

    ?  创建页表

    ?  调用平台特定的__cpu_flush函数

    ?  开启mmu

    ?  切换数据

    ?  最终跳转到start_kernel

    三、     C语言代码初始化部分

    C语言代码初始化部分主要负责建立结点和内存域的数据结构、初始化页表、初始化用

    于内存管理的伙伴系统。在启动过程中,尽管内存管理模块尚未初始化完成,但内核仍然需要分配内存以创建各种数据结构。bootmem分配器用于在启动阶段分配内存。所有涉及的实现都是在start_kernel函数中实现的,其中涉及到内存初始化的函数如下:

    1、 build_all_zonelist用于结点和内存域的初始化。

    在linux系统中,内存划分结点,每个结点关联到系统中的一个处理器。各个结点又划分内存域,主要包括ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM。大部分系统只有一个内存结点,下文只针对此类系统。此函数用于初始化内存的结点和内存域。内核在mm/page/page_alloc.c中定义了一个pglist_data的内存结点实例(contig_page_data)用于管理所有系统的内存。

    build_all_zonelist用于初始化结点和内存域。该函数首先调用__build_all_zonelists,此函数遍历系统的每个内存结点,针对每个内存结点调用build_zonelists(),该函数的任务是在当前处理的结点和系统中其他结点的内存域之间建立一种等级次序。接下来,依据这种次序分配内存,如果在期望的结点内存域中,没有空闲内存,就去查找相邻结点的内存域。内核定义了内存的一个层次结构关系,首先试图分配廉价的内存,如果失败,则根据访问速度和容量,逐渐尝试分配更昂贵的内存。

    高端内存最廉价,因为内核没有任何部分依赖于从该内存域分配的内存,如果高端内存用尽,对内核没有副作用,所以优先分配高端内存。

    普通内存域的情况有所不同,许多内核数据结构必须保存在该内存域,而不能放置到高端内存域,因此如果普通内存域用尽,那么内核会面临内存紧张的情况。

    DMA内存域最昂贵,因为它用于外设和系统之间的数据传输。

    举例来讲,如果内核指定想要分配高端内存域。它首先在当前结点的高端内存域寻找适当的空闲内存段,如果失败,则查看该结点的普通内存域,如果还失败,则试图在该结点的DMA内存域分配。如果在3个本地内存域都无法找到空闲内存,则查看其他结点。这种情况下,备选结点应该尽可能靠近主结点,以最小化访问非本地内存引起的性能损失。

    该函数接下来计算所有剩余的内存页,存放到全局变量vm_total_pages中。接下来如果空闲内存页太少,则关闭页的可迁移性,即page_group_by_mobility_disabled置位。页的可迁移性是内核为了避免内存碎片而在linux2.6.24中加入的新特性。该特性把内存页分为不可移动内存页、可回收页和可移动页三种类型来避免内存碎片。

    2、 mem_init用于停用bootmem分配器并迁移到伙伴系统。

    在系统初始化进行到伙伴系统分配器能够承担内存管理的责任后,必须停用bootmem

    配器。该函数遍历所有的内存域结点,对每个结点分别调用free_all_bootmem_node函数,停用bootmem分配器。该函数调用free_all_bootmem_core,首先扫描bootmem分配器位图,释放每个未用的页。到伙伴系统的接口是__free_pages_bootmem函数,该函数对每个空闲内存页调用,该函数内部依赖于标准函数__free_page。它使得这些页并入到伙伴系统的数据结构,在其中作为空闲页,用于分配管理。在页位图已经完全扫描后,它占据的内存空间也必须释放掉,此后,只有伙伴系统可用于内存分配。

    3、 初始化slab分配器。

    kmem_cache_init初始化内核用于小块内存区的分配器(slab分配器)。他在内核初始化阶段、伙伴系统启用之后调用。

    kmem_cache_init创建系统中的第一个slab缓存,以便为kmem_cache的实例提供内存,为此,内核使用一个在编译时创建的静态数据(cache_cache)。该函数接下来初始化一般性的缓存,用作kmalloc内存的来源。为此,针对所需的各个缓存长度,分别调用kmem_cache_create函数。

    4、 初始化页表,初始化bootmem分配器。

    setup_arch是特定于体系结构的,用于初始化页表,初始化bootmem分配器。该函数的执行流程图如下:


    4.1、setup_processorsetup_machine用来确定处理器类型和机器类型。

    4.2、parse_targs用来解析从uboot传递过来的tag值。在ubootdo_bootm_linux函数中,会创建传递给内核的各个tagtag的地址是由内核和uboot约定的,在uboot中是在函数board_init中将该约定好的地址保存在gd->bd->bi_boot_params中,如下所示:

    gd->bd->bi_boot_params = CFG_BOOT_PARAMS;

    在内核中,MACHINE_START定义了struct machine_desc数据结构,各成员函数在linux启动的不同时期被调用。该结构体的boot_params成员存放的就是uboot传递给内核的tag的起始地址。

    MACHINE_START(hi3520v100, "hi3520v100")

        .phys_io    = IO_SPACE_PHYS_START,

        .io_pg_offst    = (IO_ADDRESS(IO_SPACE_PHYS_START) >> 18) & 0xfffc,

        .boot_params    = PHYS_OFFSET + 0x100,

        .map_io     = hisilicon_map_io,

        .init_irq   = hisilicon_init_irq,

        .timer      = &hisilicon_timer,

        .init_machine   = hisilicon_init_machine,

    MACHINE_END

        在uboot中通过setup_memory_tags来建立内存的tag,把物理内存的起始地址和大小记录在tag里,tag头用ATAG_MEM标识。内核中有一个tagtable的表,把各种表头的标识和解析函数关联起来,关于内存的如下所示:

    __tagtable(ATAG_MEM, parse_tag_mem32);

    parse_targs找到以ATAG_MEM标识的tag后,调用parse_tag_mem32,把在uboot中填充到tag里的内存起始地址和大小填充到全局变量meminfo数组中。

    4.3、parse_cmdline解析命令行参数。

        此函数解析由uboot传进来的命令行参数。在内核中,各个命令行参数的头都与相应的解析函数一一对应,关于内存,有如下对应:

    __early_param("mem=", early_mem);

    在内核中解析命令行中以"mem="开头的命令行,找到此命令行后,调用early_mem函数解析命令行。如果内核需要管理几段不同的内存,可以在ubootbootarg环境变量中分别指定对应的内存段的起始地址和长度,如下所示:

    mem=72M@0xe2000000 mem=128M@0xe8000000

    表示内核需要管理两段不连续的内存,第一段起始地址为0xe2000000,大小为72M,另外一段起始地址为0xe8000000,大小为128M,内核通过early_mem函数分别把这两段内存写入到meminfo的两个bank中。

    4.4、paging_init函数用来初始化页表项,启用bootmem分配器。


    4.4.1、build_mem_type_table用来建立各种类型的页表选项。该函数是为了给mem_types数组中的各种类型的页表参数添加上我们的要求,主要是一级页表,二级页表,访问权限控制等标志位。

    4.4.2、prepare_page_table函数清除一级页表中无效的页表,只保留物理内存在虚拟地址空间中的映射。

    4.4.3、devicemaps_init函数用来清除VMALLOC_END之后的一级页表,分配vector page,调用mdesc->map_io来映射设备,mdesc->map_io即上文中提到的用MACHINE_START定义的结构体中的成员,即hisilicon_map_io

    4.4.4、bootmem_init函数在做一些必要的操作后,调用bootmem_init_node来初始化一级页表、启用bootmem分配器。

        Linux内核的段页表项将4GB的地址空间分成40961MB的段(section),每个段页表项是一个unsigned long型变量,占用4字节,因此段页表项占用4096*4=16K的内存空间。而全局页表项的大小为2M,如下

        #define PGDIR_SHIFT     21

    #define PGDIR_SIZE      (1UL << PGDIR_SHIFT)

    而全局页表项的数据结构pgd_t的定义如下

    typedef struct { unsigned long pgd[2]; } pgd_t;

    所以一个全局页表对应两个段页表,正好符合上述定义。

    该函数首先根据meminfo数组中的bank数目,分别调用map_memory_bank来映射各个物理内存区的段页表项。该函数调用create_mapping来执行具体的映射工作。

    建立完物理内存的页表映射后,内核会把物理内存的起始地址的页帧号放入start_pfn,物理内存的结束地址的页帧号放入end_pfn。这里要注意如果有多个内存bank时,start_pfnend_pfn之间可能有空洞,拿上文提到的例子来讲,start_pfnend_pfn之间是有内存空洞的,具体如下:

    mem=72M@0xe2000000 mem=128M@0xe8000000

    start_pfn = 0xe2000000 >> PAGE_SHIFT;

    end_pfn = (0xe8000000 + 8000000) >> PAGE_SHIFT;

    在内核启动期间,由于基于物理内存的伙伴系统尚未初始化完成,但内核仍然需要分配内存以创建各种数据结构,bootmem分配器用于在启动阶段早期分配内存。bootmem分配器是一个最先适配分配器,该分配器使用一个位图来管理内存页,位图比特位的数目与系统中物理内存页的数目相同,比特位为1表示已用页,比特位为0表示空闲页。在需要分配内存时,分配器逐位扫描位图,直至找到一个能提供足够连续内存页的位置,即所谓的最先适配位置。bootmem分配器也必须管理一些数据,内核为系统中的每个结点都提供了一个bootmem_data结构的实例,用于该用途。当然,该结构不能动态分配,只能在编译时分配给内核,在UMA系统上,只有一个bootmem_data_t实例,即contig_bootmem_data

    typedef struct bootmem_data {

        unsigned long node_boot_start;

        unsigned long node_low_pfn;

        void *node_bootmem_map;

        unsigned long last_offset;

        unsigned long last_pos;

        unsigned long last_success;

        struct list_head list;

    } bootmem_data_t;

    node_boot_start保存了系统中物理内存的第一个页帧的编号。

    node_low_pfn是系统物理内存最后一页的页帧编号。

    node_bootmem_map 是指向bootmem分配器位图所在地址的指针。

    last_pos是上一次分配的页帧编号。如果没有请求分配整个页帧,last_offset用作该页内部的偏移量,这使得bootmem分配器可以分配小于一整页的内存区。

    last_success指定位图中上一次成功分配内存的位置,新的分配由此开始。

    list:所有注册的bootmem分配器保存在一个链表中,表头是全局变量bdata_list

    内核接下来调用bootmem_bootmap_pages来计算bootmem分配器位图的大小(需要按页对齐),然后调用find_bootmap_pfn在物理内存中找到一块大小合适的内存,优先考虑内核的数据段的结尾处。接下来调用init_bootmem_node初始化bootmem分配器,该函数调用init_bootmem_core来填充bootmem_data_t结构中各个成员。

    接下来调用free_bootmem_node进而调用free_bootmem_core把整个位图清零,把所有内存页标记为空闲页。然后在位图中分别把已用的内存页标记为1,已用的内存页包括位图占用的内存页、initrd占用的内存页、内核的代码段和数据段占用的内存,到此bootmem分配器正式建立起来了。

    最后,free_area_init_node用来填充内存结点的数据结构pglist_data中的各个成员变量,其中zones_size表示物理内存的大小,包含内存空洞,zholes_size是内存空洞的页数。pgdat是内存结点的数据结构,node_start_pfn是物理内存的起始页帧编号。该函数首先调用calculate_node_totalpages用来计算总的物理内存页帧数,包括内存空洞,保存在内存结点数据结构的node_spanned_pages成员中,然后计算去掉内存空洞后的实际物理内存页帧数,保存在node_present_pages成员中。

    由于每个内存页都需要有一个struct page的数据结构来管理,所以该函数接下来调用

    alloc_node_mem_map来创建所有物理内存页的struct page实例,指向该空间的指针不仅保存在pglist_data结构的node_mem_map中,还保存在全局变量mem_map中。

        最后,free_area_init_node函数调用free_area_init_core来初始化pglist_data实例的各个内存域。该函数负责初始化各个zone结构中的成员。在ARM Linux中,没有ZONE_HIGHMEMZONE_NORMAL初始化成0,所有可用的物理内存被放置在ZONE_DMA。主要涉及两个函数:zone_pcp_init用来初始化冷热缓存页,init_currently_empty_zone用来初始化伙伴系统的free_area列表,并将属于该内存域的所有page实例都设置成默认值。

        zone_pcp_init负责初始化冷热缓存页。struct zonepageset成员用于实现冷热页分配器。内核说页是热的,意味着页已经加载到了CPU高速缓存,与在内存中的页相比,其数据能够更快的访问。相反,冷页则不在高速缓存中。在多处理器系统上每个CPU都有一个或多个高速缓存,各个CPU的管理必须是独立的。pageset是一个数组,其容量与系统能够容纳的CPU数目的最大值相同。

        struct zone{

            ………

            Struct per_cpu_pageset pageset[NR_CPUS];

            ………

    }

    NR_CPUS是一个可以在编译时配置的宏常数。在单处理器上其值总是1,针对SMP系统编译的内核中,其值可能是232之间。数组元素的类型为per_cpu_pageset,定义如下

    struct per_cpu_pageset {

        struct per_cpu_pages pcp[2];    /* 0: hot.  1: cold */

    } ____cacheline_aligned_in_smp;

    该结构有两个数组项,第一项管理热页,第二项管理冷页。per_cpu_pageset结构如下:

    struct per_cpu_pages {

        int count;

        int high;

        int batch;

        struct list_head list;

    };

    count记录了与该列表相关的页的数目。

    high是页数上限的水印值,在需要的情况下清空列表。

    batch是每次添加页数的值。

    zone_pcp_init函数首先用zone_batchsize计算出批量添加页的大小,保存在batch中,然后遍历系统中所有CPU,同时调用setup_pageset填充每个per_cpu_pageset实例的常量。根据计算得到的batch大约相当于内存域中页数的0.25‰。

    init_currently_empty_zone用来初始化与伙伴系统相关的free_aera列表。该函数首先调用memmap_init_zone初始化上文分配好的保存在全局变量mem_map中的物理内存的struct page实例。然后调用zone_init_free_lists初始化free_aera空闲列表。空闲页的数目free_area.nr_free当前仍然规定为0,直至停用bootmem分配器、普通的伙伴系统分配器生效时,才会设置正确的数值。

    整个内核地址空间的划分请参见下图:


    图中PAGE_OFFSET=0xc0000000TEXT_OFFSET=0x00008000arch/arm/makefile中指定,swapper_pg_dir=0x00004000head.S中指定。

        地址空间的第一段用于将系统的所有物理内存页映射到内核的虚拟地址空间中。由于内核地址空间从偏移量0xc0000000开始,即3GiB,所以每个虚拟地址x都对应于物理地址x-0xc0000000,因此这是一个简单的线性偏移。

        直接映射区:从PAGE_OFFSET(0xc0000000)开始到high_memory的地址空间,在内核调用bootmem_init函数中,把high_memory的值设置为实际物理内存的结束地址对应的虚拟地址,最大不超过896M。当实际物理内存大于896M时,超出的部分映射为高端内存区。物理内存的起始地址的16K未用,从swapper_pg_dir0xc000800016K存放段地址的页表项内容,前文已经描述过了。从0xc0008000开始存放内核的代码段、初始化的数据段和未初始化的数据段。紧接着内核的代码段和数据段的是bootmem分配器的位图区域,上文中页提到过。

        VMALLOC区:虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配。该机制通常用于用户过程,内核自身会试图尽力避免非连续的物理内存。但在已经运行了很长时间的系统上,在内核需要物理内存时,可能出现可用内存不连续的情况。此类情况,主要出现在动态加载模块时。vmalloc区域在何处结束取决于是否启用了高端内存支持。如果没有启用,那么就不需要持久映射区,因为整个物理内存都是可以直接映射的。因此根据配置的不同,该区域结束于持久内核映射或固定映射区域的起始处,中间总会留下两页,作为vmalloc区与这两个区域之间的保护措施。

        持久映射区:用于将高端内存域中的非持久页映射到内核。

        固定映射区:是与物理地址空间中的固定页关联的虚拟地址空间页,但具体关联的页帧可以自由选择。它通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间的关联可以自行定义。

        在直接映射区和vmalloc区域之间有一个8M的缺口,这个缺口可用作针对任何内核故障的保护措施。如果访问越界地址,则访问失败并生成一个异常,报告该错误。如果vmalloc区域紧接着直接映射,那么访问将成功而不会注意到错误。

    到此,整个linux的内存管理子系统初始化完成了,具体的细节部分请参考代码阅读体会。


    from:http://sunjiangang.blog.chinaunix.net/uid-9543173-id-3568385.html

    展开全文
  • C语言内存初始化

    千次阅读 2016-05-08 02:17:47
    我们编写C语言的时候需要给变量申请一块内存区域,当我们创建一个内存区域的时候,内存中的数据十有八九是乱七八糟的(因为其他代码用过后遗留的数据并没有及时清掉) 例如: int main() { char str[10];//分配的...

    我们编写C语言的时候需要给变量申请一块内存区域,当我们创建一个内存区域的时候,内存中的数据十有八九是乱七八糟的(因为其他代码用过后遗留的数据并没有及时清掉)

    例如:

     

    int main()
    {
        char str[10];//分配的10个字节的内存可能被用过;
        printf("%s\n",str);//这个代码打印出来的可能就是乱码,因为printf的%s是“打印一直遇到'\0'"
        return 0;
    }

     

    那么,有什么方法可以解决呢?

     

    这里有两种解决问题的方法:

    第一种:使用memset函数为新申请的内存做初始化工作

     

    memset(void*,要填充的数据,要填充的字节个数)

     

     

    int main()
    {
        char str[10];//编译器自动分配10个字节的内存大小
        memset(str,0,sizeof(str));//sizeof()计算字节大小
        printf("%s\n",str);
        return 0;
    }

     

     

    第二种:

    char str[10] ={0}; 对于长度为10字节的这段内容全部填充为0。原理:int num[10]={6,8,5};//前三个分别填充6、8和5,之后都填充为0;

     

    最后补充:

    void *memset(void *str, int ch, size_t n);

    函数解释:将str中前n个字节 (typedef unsigned int size_t )用 ch 替换并返回 str 。memset:作用是在一段内存块中填充某个给定的值,它是对较大的结构体数组进行清零操作的一种最快方法。

    memset是计算机中C/C++语言函数。将s所指向的某一块内存中的前n个 字节的内容全部设置为ch指定的ASCII值, 第一个值为指定的内存地址,块的大小由第三个参数指定,这个函数通常为新申请的内存做初始化工作, 其返回值为指向s的指针

     

    展开全文
  • 内存初始化

    千次阅读 2011-03-09 11:36:00
    文中将为您提供如何管理内存的细节,然后将进一步展示如何手工管理内存,如何使用引用计数或者内存池来半手工地管理内存,以及如何使用垃圾收集自动管理内存。 为什么必须管理内存 <br />内存管理是计算机...
    软件说明:

    文将对 Linux™ 程序员可以使用的内存管理技术进行概述,虽然关注的重点是 C 语言,但同样也适用于其他语言。文中将为您提供如何管理内存的细节,然后将进一步展示如何手工管理内存,如何使用引用计数或者内存池来半手工地管理内存,以及如何使用垃圾收集自动管理内存。
    为什么必须管理内存

    内存管理是计算机编程最为基本的领域之一。在很多脚本语言中,您不必担心内存是如何管理的,这并不能使得内存管理的重要性有一点点降低。对实际编程来说,理解您的内存管理器的能力与局限性至关重要。在大部分系统语言中,比如 C 和 C++,您必须进行内存管理。本文将介绍手工的、半手工的以及自动的内存管理实践的基本概念。

    追溯到在 Apple II 上进行汇编语言编程的时代,那时内存管理还不是个大问题。您实际上在运行整个系统。系统有多少内存,您就有多少内存。您甚至不必费心思去弄明白它有多少内存,因为每一台机器的内存数量都相同。所以,如果内存需要非常固定,那么您只需要选择一个内存范围并使用它即可。

    不过,即使是在这样一个简单的计算机中,您也会有问题,尤其是当您不知道程序的每个部分将需要多少内存时。如果您的空间有限,而内存需求是变化的,那么您需要一些方法来满足这些需求:

    确定您是否有足够的内存来处理数据。
    从可用的内存中获取一部分内存。
    向可用内存池(pool)中返回部分内存,以使其可以由程序的其他部分或者其他程序使用。

    实现这些需求的程序库称为 分配程序(allocators),因为它们负责分配和回收内存。程序的动态性越强,内存管理就越重要,您的内存分配程序的选择也就更重要。让我们来了解可用于内存管理的不同方法,它们的好处与不足,以及它们最适用的情形。






    回页首




    C 风格的内存分配程序

    C 编程语言提供了两个函数来满足我们的三个需求:

    malloc:该函数分配给定的字节数,并返回一个指向它们的指针。如果没有足够的可用内存,那么它返回一个空指针。
    free:该函数获得指向由 malloc 分配的内存片段的指针,并将其释放,以便以后的程序或操作系统使用(实际上,一些 malloc 实现只能将内存归还给程序,而无法将内存归还给操作系统)。

    物理内存和虚拟内存

    要理解内存在程序中是如何分配的,首先需要理解如何将内存从操作系统分配给程序。计算机上的每一个进程都认为自己可以访问所有的物理内存。显然,由于同时在运行多个程序,所以每个进程不可能拥有全部内存。实际上,这些进程使用的是 虚拟内存。

    只是作为一个例子,让我们假定您的程序正在访问地址为 629 的内存。不过,虚拟内存系统不需要将其存储在位置为 629 的 RAM 中。实际上,它甚至可以不在 RAM 中 —— 如果物理 RAM 已经满了,它甚至可能已经被转移到硬盘上!由于这类地址不必反映内存所在的物理位置,所以它们被称为虚拟内存。操作系统维持着一个虚拟地址到物理地址的转换的表,以便计算机硬件可以正确地响应地址请求。并且,如果地址在硬盘上而不是在 RAM 中,那么操作系统将暂时停止您的进程,将其他内存转存到硬盘中,从硬盘上加载被请求的内存,然后再重新启动您的进程。这样,每个进程都获得了自己可以使用的地址空间,可以访问比您物理上安装的内存更多的内存。

    在 32-位 x86 系统上,每一个进程可以访问 4 GB 内存。现在,大部分人的系统上并没有 4 GB 内存,即使您将 swap 也算上, 每个进程所使用的内存也肯定少于 4 GB。因此,当加载一个进程时,它会得到一个取决于某个称为 系统中断点(system break)的特定地址的初始内存分配。该地址之后是未被映射的内存 —— 用于在 RAM 或者硬盘中没有分配相应物理位置的内存。因此,如果一个进程运行超出了它初始分配的内存,那么它必须请求操作系统“映射进来(map in)”更多的内存。(映射是一个表示一一对应关系的数学术语 —— 当内存的虚拟地址有一个对应的物理地址来存储内存内容时,该内存将被映射。)

    基于 UNIX 的系统有两个可映射到附加内存中的基本系统调用:

    brk: brk() 是一个非常简单的系统调用。还记得系统中断点吗?该位置是进程映射的内存边界。 brk() 只是简单地将这个位置向前或者向后移动,就可以向进程添加内存或者从进程取走内存。
    mmap: mmap(),或者说是“内存映像”,类似于 brk(),但是更为灵活。首先,它可以映射任何位置的内存,而不单单只局限于进程。其次,它不仅可以将虚拟地址映射到物理的 RAM 或者 swap,它还可以将它们映射到文件和文件位置,这样,读写内存将对文件中的数据进行读写。不过,在这里,我们只关心 mmap 向进程添加被映射的内存的能力。 munmap() 所做的事情与 mmap() 相反。

    如您所见, brk() 或者 mmap() 都可以用来向我们的进程添加额外的虚拟内存。在我们的例子中将使用 brk(),因为它更简单,更通用。

    实现一个简单的分配程序

    如果您曾经编写过很多 C 程序,那么您可能曾多次使用过 malloc() 和 free()。不过,您可能没有用一些时间去思考它们在您的操作系统中是如何实现的。本节将向您展示 malloc 和 free 的一个最简化实现的代码,来帮助说明管理内存时都涉及到了哪些事情。

    要试着运行这些示例,需要先 复制本代码清单,并将其粘贴到一个名为 malloc.c 的文件中。接下来,我将一次一个部分地对该清单进行解释。

    在大部分操作系统中,内存分配由以下两个简单的函数来处理:

    void *malloc(long numbytes):该函数负责分配 numbytes 大小的内存,并返回指向第一个字节的指针。
    void free(void *firstbyte):如果给定一个由先前的 malloc 返回的指针,那么该函数会将分配的空间归还给进程的“空闲空间”。
    malloc_init 将是初始化内存分配程序的函数。它要完成以下三件事:将分配程序标识为已经初始化,找到系统中最后一个有效内存地址,然后建立起指向我们管理的内存的指针。这三个变量都是全局变量:


    清单 1. 我们的简单分配程序的全局变量

    int has_initialized = 0;
    void *managed_memory_start;
    void *last_valid_address;



    如前所述,被映射的内存的边界(最后一个有效地址)常被称为系统中断点或者 当前中断点。在很多 UNIX® 系统中,为了指出当前系统中断点,必须使用 sbrk(0) 函数。 sbrk 根据参数中给出的字节数移动当前系统中断点,然后返回新的系统中断点。使用参数 0 只是返回当前中断点。这里是我们的 malloc 初始化代码,它将找到当前中断点并初始化我们的变量:


    清单 2. 分配程序初始化函数

    /* Include the sbrk function */
    #include <unistd.h>
    void malloc_init()
    {
    /* grab the last valid address from the OS */
    last_valid_address = sbrk(0);
    /* we don't have any memory to manage yet, so
    *just set the beginning to be last_valid_address
    */
    managed_memory_start = last_valid_address;
    /* Okay, we're initialized and ready to go */
    has_initialized = 1;
    }



    现在,为了完全地管理内存,我们需要能够追踪要分配和回收哪些内存。在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块。因此, malloc 返回的每块内存的起始处首先要有这个结构:


    清单 3. 内存控制块结构定义

    struct mem_control_block {
    int is_available;
    int size;
    };



    现在,您可能会认为当程序调用 malloc 时这会引发问题 —— 它们如何知道这个结构?答案是它们不必知道;在返回指针之前,我们会将其移动到这个结构之后,把它隐藏起来。这使得返回的指针指向没有用于任何其他用途的内存。那样,从调用程序的角度来看,它们所得到的全部是空闲的、开放的内存。然后,当通过 free() 将该指针传递回来时,我们只需要倒退几个内存字节就可以再次找到这个结构。

    在讨论分配内存之前,我们将先讨论释放,因为它更简单。为了释放内存,我们必须要做的惟一一件事情就是,获得我们给出的指针,回退 sizeof(struct mem_control_block) 个字节,并将其标记为可用的。这里是对应的代码:


    清单 4. 解除分配函数

    void free(void *firstbyte) {
    struct mem_control_block *mcb;
    /* Backup from the given pointer to find the
    * mem_control_block
    */
    mcb = firstbyte - sizeof(struct mem_control_block);
    /* Mark the block as being available */
    mcb->is_available = 1;
    /* That's It! We're done. */
    return;
    }



    如您所见,在这个分配程序中,内存的释放使用了一个非常简单的机制,在固定时间内完成内存释放。分配内存稍微困难一些。以下是该算法的略述:


    清单 5. 主分配程序的伪代码

    1. If our allocator has not been initialized, initialize it.
    2. Add sizeof(struct mem_control_block) to the size requested.
    3. start at managed_memory_start.
    4. Are we at last_valid address?
    5. If we are:
    A. We didn't find any existing space that was large enough
    -- ask the operating system for more and return that.
    6. Otherwise:
    A. Is the current space available (check is_available from
    the mem_control_block)?
    B. If it is:
    i) Is it large enough (check "size" from the
    mem_control_block)?
    ii) If so:
    a. Mark it as unavailable
    b. Move past mem_control_block and return the
    pointer
    iii) Otherwise:
    a. Move forward "size" bytes
    b. Go back go step 4
    C. Otherwise:
    i) Move forward "size" bytes
    ii) Go back to step 4



    我们主要使用连接的指针遍历内存来寻找开放的内存块。这里是代码:


    清单 6. 主分配程序

    void *malloc(long numbytes) {
    /* Holds where we are looking in memory */
    void *current_location;
    /* This is the same as current_location, but cast to a
    * memory_control_block
    */
    struct mem_control_block *current_location_mcb;
    /* This is the memory location we will return. It will
    * be set to 0 until we find something suitable
    */
    void *memory_location;
    /* Initialize if we haven't already done so */
    if(! has_initialized) {
    malloc_init();
    }
    /* The memory we search for has to include the memory
    * control block, but the users of malloc don't need
    * to know this, so we'll just add it in for them.
    */
    numbytes = numbytes + sizeof(struct mem_control_block);
    /* Set memory_location to 0 until we find a suitable
    * location
    */
    memory_location = 0;
    /* Begin searching at the start of managed memory */
    current_location = managed_memory_start;
    /* Keep going until we have searched all allocated space */
    while(current_location != last_valid_address)
    {
    /* current_location and current_location_mcb point
    * to the same address. However, current_location_mcb
    * is of the correct type, so we can use it as a struct.
    * current_location is a void pointer so we can use it
    * to calculate addresses.
    */
    current_location_mcb =
    (struct mem_control_block *)current_location;
    if(current_location_mcb->is_available)
    {
    if(current_location_mcb->size >= numbytes)
    {
    /* Woohoo! We've found an open,
    * appropriately-size location.
    */
    /* It is no longer available */
    current_location_mcb->is_available = 0;
    /* We own it */
    memory_location = current_location;
    /* Leave the loop */
    break;
    }
    }
    /* If we made it here, it's because the Current memory
    * block not suitable; move to the next one
    */
    current_location = current_location +
    current_location_mcb->size;
    }
    /* If we still don't have a valid location, we'll
    * have to ask the operating system for more memory
    */
    if(! memory_location)
    {
    /* Move the program break numbytes further */
    sbrk(numbytes);
    /* The new memory will be where the last valid
    * address left off
    */
    memory_location = last_valid_address;
    /* We'll move the last valid address forward
    * numbytes
    */
    last_valid_address = last_valid_address + numbytes;
    /* We need to initialize the mem_control_block */
    current_location_mcb = memory_location;
    current_location_mcb->is_available = 0;
    current_location_mcb->size = numbytes;
    }
    /* Now, no matter what (well, except for error conditions),
    * memory_location has the address of the memory, including
    * the mem_control_block
    */
    /* Move the pointer past the mem_control_block */
    memory_location = memory_location + sizeof(struct mem_control_block);
    /* Return the pointer */
    return memory_location;
    }



    这就是我们的内存管理器。现在,我们只需要构建它,并在程序中使用它即可。

    运行下面的命令来构建 malloc 兼容的分配程序(实际上,我们忽略了 realloc() 等一些函数,不过, malloc() 和 free() 才是最主要的函数):


    清单 7. 编译分配程序

    gcc -shared -fpic malloc.c -o malloc.so



    该程序将生成一个名为 malloc.so 的文件,它是一个包含有我们的代码的共享库。

    在 UNIX 系统中,现在您可以用您的分配程序来取代系统的 malloc(),做法如下:


    清单 8. 替换您的标准的 malloc

    LD_PRELOAD=/path/to/malloc.so
    export LD_PRELOAD



    LD_PRELOAD 环境变量使动态链接器在加载任何可执行程序之前,先加载给定的共享库的符号。它还为特定库中的符号赋予优先权。因此,从现在起,该会话中的任何应用程序都将使用我们的 malloc(),而不是只有系统的应用程序能够使用。有一些应用程序不使用 malloc(),不过它们是例外。其他使用 realloc() 等其他内存管理函数的应用程序,或者错误地假定 malloc() 内部行为的那些应用程序,很可能会崩溃。ash shell 似乎可以使用我们的新 malloc() 很好地工作。

    如果您想确保 malloc() 正在被使用,那么您应该通过向函数的入口点添加 write() 调用来进行测试。

    我们的内存管理器在很多方面都还存在欠缺,但它可以有效地展示内存管理需要做什么事情。它的某些缺点包括:

    由于它对系统中断点(一个全局变量)进行操作,所以它不能与其他分配程序或者 mmap 一起使用。
    当分配内存时,在最坏的情形下,它将不得不遍历 全部进程内存;其中可能包括位于硬盘上的很多内存,这意味着操作系统将不得不花时间去向硬盘移入数据和从硬盘中移出数据。
    没有很好的内存不足处理方案( malloc 只假定内存分配是成功的)。
    它没有实现很多其他的内存函数,比如 realloc()。
    由于 sbrk() 可能会交回比我们请求的更多的内存,所以在堆(heap)的末端会遗漏一些内存。
    虽然 is_available 标记只包含一位信息,但它要使用完整的 4-字节 的字。
    分配程序不是线程安全的。
    分配程序不能将空闲空间拼合为更大的内存块。
    分配程序的过于简单的匹配算法会导致产生很多潜在的内存碎片。
    我确信还有很多其他问题。这就是为什么它只是一个例子!

    其他 malloc 实现

    malloc() 的实现有很多,这些实现各有优点与缺点。在设计一个分配程序时,要面临许多需要折衷的选择,其中包括:

    分配的速度。
    回收的速度。
    有线程的环境的行为。
    内存将要被用光时的行为。
    局部缓存。
    簿记(Bookkeeping)内存开销。
    虚拟内存环境中的行为。
    小的或者大的对象。
    实时保证。

    每一个实现都有其自身的优缺点集合。在我们的简单的分配程序中,分配非常慢,而回收非常快。另外,由于它在使用虚拟内存系统方面较差,所以它最适于处理大的对象。

    还有其他许多分配程序可以使用。其中包括:

    Doug Lea Malloc:Doug Lea Malloc 实际上是完整的一组分配程序,其中包括 Doug Lea 的原始分配程序,GNU libc 分配程序和 ptmalloc。 Doug Lea 的分配程序有着与我们的版本非常类似的基本结构,但是它加入了索引,这使得搜索速度更快,并且可以将多个没有被使用的块组合为一个大的块。它还支持缓存,以便更快地再次使用最近释放的内存。 ptmalloc 是 Doug Lea Malloc 的一个扩展版本,支持多线程。在本文后面的 参考资料部分中,有一篇描述 Doug Lea 的 Malloc 实现的文章。
    BSD Malloc:BSD Malloc 是随 4.2 BSD 发行的实现,包含在 FreeBSD 之中,这个分配程序可以从预先确实大小的对象构成的池中分配对象。它有一些用于对象大小的 size 类,这些对象的大小为 2 的若干次幂减去某一常数。所以,如果您请求给定大小的一个对象,它就简单地分配一个与之匹配的 size 类。这样就提供了一个快速的实现,但是可能会浪费内存。在 参考资料部分中,有一篇描述该实现的文章。
    Hoard:编写 Hoard 的目标是使内存分配在多线程环境中进行得非常快。因此,它的构造以锁的使用为中心,从而使所有进程不必等待分配内存。它可以显著地加快那些进行很多分配和回收的多线程进程的速度。在 参考资料部分中,有一篇描述该实现的文章。

    众多可用的分配程序中最有名的就是上述这些分配程序。如果您的程序有特别的分配需求,那么您可能更愿意编写一个定制的能匹配您的程序内存分配方式的分配程序。不过,如果不熟悉分配程序的设计,那么定制分配程序通常会带来比它们解决的问题更多的问题。要获得关于该主题的适当的介绍,请参阅 Donald Knuth 撰写的 The Art of Computer Programming Volume 1: Fundamental Algorithms 中的第 2.5 节“Dynamic Storage Allocation”(请参阅 参考资料中的链接)。它有点过时,因为它没有考虑虚拟内存环境,不过大部分算法都是基于前面给出的函数。

    在 C++ 中,通过重载 operator new(),您可以以每个类或者每个模板为单位实现自己的分配程序。在 Andrei Alexandrescu 撰写的 Modern C++ Design 的第 4 章(“Small Object Allocation”)中,描述了一个小对象分配程序(请参阅 参考资料中的链接)。

    基于 malloc() 的内存管理的缺点

    不只是我们的内存管理器有缺点,基于 malloc() 的内存管理器仍然也有很多缺点,不管您使用的是哪个分配程序。对于那些需要保持长期存储的程序使用 malloc() 来管理内存可能会非常令人失望。如果您有大量的不固定的内存引用,经常难以知道它们何时被释放。生存期局限于当前函数的内存非常容易管理,但是对于生存期超出该范围的内存来说,管理内存则困难得多。而且,关于内存管理是由进行调用的程序还是由被调用的函数来负责这一问题,很多 API 都不是很明确。

    因为管理内存的问题,很多程序倾向于使用它们自己的内存管理规则。C++ 的异常处理使得这项任务更成问题。有时好像致力于管理内存分配和清理的代码比实际完成计算任务的代码还要多!因此,我们将研究内存管理的其他选择。






    回页首




    半自动内存管理策略

    引用计数

    引用计数是一种 半自动(semi-automated)的内存管理技术,这表示它需要一些编程支持,但是它不需要您确切知道某一对象何时不再被使用。引用计数机制为您完成内存管理任务。

    在引用计数中,所有共享的数据结构都有一个域来包含当前活动“引用”结构的次数。当向一个程序传递一个指向某个数据结构指针时,该程序会将引用计数增加 1。实质上,您是在告诉数据结构,它正在被存储在多少个位置上。然后,当您的进程完成对它的使用后,该程序就会将引用计数减少 1。结束这个动作之后,它还会检查计数是否已经减到零。如果是,那么它将释放内存。

    这样做的好处是,您不必追踪程序中某个给定的数据结构可能会遵循的每一条路径。每次对其局部的引用,都将导致计数的适当增加或减少。这样可以防止在使用数据结构时释放该结构。不过,当您使用某个采用引用计数的数据结构时,您必须记得运行引用计数函数。另外,内置函数和第三方的库不会知道或者可以使用您的引用计数机制。引用计数也难以处理发生循环引用的数据结构。

    要实现引用计数,您只需要两个函数 —— 一个增加引用计数,一个减少引用计数并当计数减少到零时释放内存。

    一个示例引用计数函数集可能看起来如下所示:


    清单 9. 基本的引用计数函数

    /* Structure Definitions*/
    /* Base structure that holds a refcount */
    struct refcountedstruct
    {
    int refcount;
    }
    /* All refcounted structures must mirror struct
    * refcountedstruct for their first variables
    */
    /* Refcount maintenance functions */
    /* Increase reference count */
    void REF(void *data)
    {
    struct refcountedstruct *rstruct;
    rstruct = (struct refcountedstruct *) data;
    rstruct->refcount++;
    }
    /* Decrease reference count */
    void UNREF(void *data)
    {
    struct refcountedstruct *rstruct;
    rstruct = (struct refcountedstruct *) data;
    rstruct->refcount--;
    /* Free the structure if there are no more users */
    if(rstruct->refcount == 0)
    {
    free(rstruct);
    }
    }



    REF 和 UNREF 可能会更复杂,这取决于您想要做的事情。例如,您可能想要为多线程程序增加锁,那么您可能想扩展 refcountedstruct,使它同样包含一个指向某个在释放内存之前要调用的函数的指针(类似于面向对象语言中的析构函数 —— 如果您的结构中包含这些指针,那么这是 必需的)。

    当使用 REF 和 UNREF 时,您需要遵守这些指针的分配规则:

    UNREF 分配前左端指针(left-hand-side pointer)指向的值。
    REF 分配后左端指针(left-hand-side pointer)指向的值。

    在传递使用引用计数的结构的函数中,函数需要遵循以下这些规则:

    在函数的起始处 REF 每一个指针。
    在函数的结束处 UNREF 第一个指针。

    以下是一个使用引用计数的生动的代码示例:


    清单 10. 使用引用计数的示例

    /* EXAMPLES OF USAGE */
    /* Data type to be refcounted */
    struct mydata
    {
    int refcount; /* same as refcountedstruct */
    int datafield1; /* Fields specific to this struct */
    int datafield2;
    /* other declarations would go here as appropriate */
    };
    /* Use the functions in code */
    void dosomething(struct mydata *data)
    {
    REF(data);
    /* Process data */
    /* when we are through */
    UNREF(data);
    }
    struct mydata *globalvar1;
    /* Note that in this one, we don't decrease the
    * refcount since we are maintaining the reference
    * past the end of the function call through the
    * global variable
    */
    void storesomething(struct mydata *data)
    {
    REF(data); /* passed as a parameter */
    globalvar1 = data;
    REF(data); /* ref because of Assignment */
    UNREF(data); /* Function finished */
    }



    由于引用计数是如此简单,大部分程序员都自已去实现它,而不是使用库。不过,它们依赖于 malloc 和 free 等低层的分配程序来实际地分配和释放它们的内存。

    在 Perl 等高级语言中,进行内存管理时使用引用计数非常广泛。在这些语言中,引用计数由语言自动地处理,所以您根本不必担心它,除非要编写扩展模块。由于所有内容都必须进行引用计数,所以这会对速度产生一些影响,但它极大地提高了编程的安全性和方便性。以下是引用计数的益处:

    实现简单。
    易于使用。
    由于引用是数据结构的一部分,所以它有一个好的缓存位置。

    不过,它也有其不足之处:

    要求您永远不要忘记调用引用计数函数。
    无法释放作为循环数据结构的一部分的结构。
    减缓几乎每一个指针的分配。
    尽管所使用的对象采用了引用计数,但是当使用异常处理(比如 try 或 setjmp()/ longjmp())时,您必须采取其他方法。
    需要额外的内存来处理引用。
    引用计数占用了结构中的第一个位置,在大部分机器中最快可以访问到的就是这个位置。
    在多线程环境中更慢也更难以使用。

    C++ 可以通过使用 智能指针(smart pointers)来容忍程序员所犯的一些错误,智能指针可以为您处理引用计数等指针处理细节。不过,如果不得不使用任何先前的不能处理智能指针的代码(比如对 C 库的联接),实际上,使用它们的后果通实比不使用它们更为困难和复杂。因此,它通常只是有益于纯 C++ 项目。如果您想使用智能指针,那么您实在应该去阅读 Alexandrescu 撰写的 Modern C++ Design 一书中的“Smart Pointers”那一章。

    内存池

    内存池是另一种半自动内存管理方法。内存池帮助某些程序进行自动内存管理,这些程序会经历一些特定的阶段,而且每个阶段中都有分配给进程的特定阶段的内存。例如,很多网络服务器进程都会分配很多针对每个连接的内存 —— 内存的最大生存期限为当前连接的存在期。Apache 使用了池式内存(pooled memory),将其连接拆分为各个阶段,每个阶段都有自己的内存池。在结束每个阶段时,会一次释放所有内存。

    在池式内存管理中,每次内存分配都会指定内存池,从中分配内存。每个内存池都有不同的生存期限。在 Apache 中,有一个持续时间为服务器存在期的内存池,还有一个持续时间为连接的存在期的内存池,以及一个持续时间为请求的存在期的池,另外还有其他一些内存池。因此,如果我的一系列函数不会生成比连接持续时间更长的数据,那么我就可以完全从连接池中分配内存,并知道在连接结束时,这些内存会被自动释放。另外,有一些实现允许注册 清除函数(cleanup functions),在清除内存池之前,恰好可以调用它,来完成在内存被清理前需要完成的其他所有任务(类似于面向对象中的析构函数)。

    要在自己的程序中使用池,您既可以使用 GNU libc 的 obstack 实现,也可以使用 Apache 的 Apache Portable Runtime。GNU obstack 的好处在于,基于 GNU 的 Linux 发行版本中默认会包括它们。Apache Portable Runtime 的好处在于它有很多其他工具,可以处理编写多平台服务器软件所有方面的事情。要深入了解 GNU obstack 和 Apache 的池式内存实现,请参阅 参考资料部分中指向这些实现的文档的链接。

    下面的假想代码列表展示了如何使用 obstack:


    清单 11. obstack 的示例代码

    #include <obstack.h>
    #include <stdlib.h>
    /* Example code listing for using obstacks */
    /* Used for obstack macros (xmalloc is
    a malloc function that exits if memory
    is exhausted */
    #define obstack_chunk_alloc xmalloc
    #define obstack_chunk_free free
    /* Pools */
    /* Only permanent allocations should go in this pool */
    struct obstack *global_pool;
    /* This pool is for per-connection data */
    struct obstack *connection_pool;
    /* This pool is for per-request data */
    struct obstack *request_pool;
    void allocation_failed()
    {
    exit(1);
    }
    int main()
    {
    /* Initialize Pools */
    global_pool = (struct obstack *)
    xmalloc (sizeof (struct obstack));
    obstack_init(global_pool);
    connection_pool = (struct obstack *)
    xmalloc (sizeof (struct obstack));
    obstack_init(connection_pool);
    request_pool = (struct obstack *)
    xmalloc (sizeof (struct obstack));
    obstack_init(request_pool);
    /* Set the error handling function */
    obstack_alloc_failed_handler = &allocation_failed;
    /* Server main loop */
    while(1)
    {
    wait_for_connection();
    /* We are in a connection */
    while(more_requests_available())
    {
    /* Handle request */
    handle_request();
    /* Free all of the memory allocated
    * in the request pool
    */
    obstack_free(request_pool, NULL);
    }
    /* We're finished with the connection, time
    * to free that pool
    */
    obstack_free(connection_pool, NULL);
    }
    }
    int handle_request()
    {
    /* Be sure that all object allocations are allocated
    * from the request pool
    */
    int bytes_i_need = 400;
    void *data1 = obstack_alloc(request_pool, bytes_i_need);
    /* Do stuff to process the request */
    /* return */
    return 0;
    }



    基本上,在操作的每一个主要阶段结束之后,这个阶段的 obstack 会被释放。不过,要注意的是,如果一个过程需要分配持续时间比当前阶段更长的内存,那么它也可以使用更长期限的 obstack,比如连接或者全局内存。传递给 obstack_free() 的 NULL 指出它应该释放 obstack 的全部内容。可以用其他的值,但是它们通常不怎么实用。

    使用池式内存分配的益处如下所示:

    应用程序可以简单地管理内存。
    内存分配和回收更快,因为每次都是在一个池中完成的。分配可以在 O(1) 时间内完成,释放内存池所需时间也差不多(实际上是 O(n) 时间,不过在大部分情况下会除以一个大的因数,使其变成 O(1))。
    可以预先分配错误处理池(Error-handling pools),以便程序在常规内存被耗尽时仍可以恢复。
    有非常易于使用的标准实现。

    池式内存的缺点是:

    内存池只适用于操作可以分阶段的程序。
    内存池通常不能与第三方库很好地合作。
    如果程序的结构发生变化,则不得不修改内存池,这可能会导致内存管理系统的重新设计。
    您必须记住需要从哪个池进行分配。另外,如果在这里出错,就很难捕获该内存池。





    回页首




    垃圾收集

    垃圾收集(Garbage collection)是全自动地检测并移除不再使用的数据对象。垃圾收集器通常会在当可用内存减少到少于一个具体的阈值时运行。通常,它们以程序所知的可用的一组“基本”数据 —— 栈数据、全局变量、寄存器 —— 作为出发点。然后它们尝试去追踪通过这些数据连接到每一块数据。收集器找到的都是有用的数据;它没有找到的就是垃圾,可以被销毁并重新使用这些无用的数据。为了有效地管理内存,很多类型的垃圾收集器都需要知道数据结构内部指针的规划,所以,为了正确运行垃圾收集器,它们必须是语言本身的一部分。

    收集器的类型


    复制(copying): 这些收集器将内存存储器分为两部分,只允许数据驻留在其中一部分上。它们定时地从“基本”的元素开始将数据从一部分复制到另一部分。内存新近被占用的部分现在成为活动的,另一部分上的所有内容都认为是垃圾。另外,当进行这项复制操作时,所有指针都必须被更新为指向每个内存条目的新位置。因此,为使用这种垃圾收集方法,垃圾收集器必须与编程语言集成在一起。
    标记并清理(Mark and sweep):每一块数据都被加上一个标签。不定期的,所有标签都被设置为 0,收集器从“基本”的元素开始遍历数据。当它遇到内存时,就将标签标记为 1。最后没有被标记为 1 的所有内容都认为是垃圾,以后分配内存时会重新使用它们。
    增量的(Incremental):增量垃圾收集器不需要遍历全部数据对象。因为在收集期间的突然等待,也因为与访问所有当前数据相关的缓存问题(所有内容都不得不被页入(page-in)),遍历所有内存会引发问题。增量收集器避免了这些问题。
    保守的(Conservative):保守的垃圾收集器在管理内存时不需要知道与数据结构相关的任何信息。它们只查看所有数据类型,并假定它们 可以全部都是指针。所以,如果一个字节序列可以是一个指向一块被分配的内存的指针,那么收集器就将其标记为正在被引用。有时没有被引用的内存会被收集,这样会引发问题,例如,如果一个整数域中包含一个值,该值是已分配内存的地址。不过,这种情况极少发生,而且它只会浪费少量内存。保守的收集器的优势是,它们可以与任何编程语言相集成。

    Hans Boehm 的保守垃圾收集器是可用的最流行的垃圾收集器之一,因为它是免费的,而且既是保守的又是增量的,可以使用 --enable-redirect-malloc 选项来构建它,并且可以将它用作系统分配程序的简易替代者(drop-in replacement)(用 malloc/ free 代替它自己的 API)。实际上,如果这样做,您就可以使用与我们在示例分配程序中所使用的相同的 LD_PRELOAD 技巧,在系统上的几乎任何程序中启用垃圾收集。如果您怀疑某个程序正在泄漏内存,那么您可以使用这个垃圾收集器来控制进程。在早期,当 Mozilla 严重地泄漏内存时,很多人在其中使用了这项技术。这种垃圾收集器既可以在 Windows® 下运行,也可以在 UNIX 下运行。

    垃圾收集的一些优点:

    您永远不必担心内存的双重释放或者对象的生命周期。
    使用某些收集器,您可以使用与常规分配相同的 API。

    其缺点包括:

    使用大部分收集器时,您都无法干涉何时释放内存。
    在多数情况下,垃圾收集比其他形式的内存管理更慢。
    垃圾收集错误引发的缺陷难于调试。
    如果您忘记将不再使用的指针设置为 null,那么仍然会有内存泄漏。






    回页首




    结束语

    一切都需要折衷:性能、易用、易于实现、支持线程的能力等,这里只列出了其中的一些。为了满足项目的要求,有很多内存管理模式可以供您使用。每种模式都有大量的实现,各有其优缺点。对很多项目来说,使用编程环境默认的技术就足够了,不过,当您的项目有特殊的需要时,了解可用的选择将会有帮助。下表对比了本文中涉及的内存管理策略。

    表 1. 内存分配策略的对比

    策略 分配速度 回收速度 局部缓存 易用性 通用性 实时可用 SMP 线程友好
    定制分配程序 取决于实现 取决于实现 取决于实现 很难 无 取决于实现 取决于实现
    简单分配程序 内存使用少时较快 很快 差 容易 高 否 否
    GNU malloc 中 快 中 容易 高 否 中
    Hoard 中 中 中 容易 高 否 是
    引用计数 N/A N/A 非常好 中 中 是(取决于 malloc 实现) 取决于实现
    池 中 非常快 极好 中 中 是(取决于 malloc 实现) 取决于实现
    垃圾收集 中(进行收集时慢) 中 差 中 中 否 几乎不
    增量垃圾收集 中 中 中 中 中 否 几乎不
    增量保守垃圾收集 中 中 中 容易 高 否 几乎不





    参考资料

    您可以参阅本文在 developerWorks 全球站点上的 英文原文。

    Web 上的文档

    GNU C Library 手册的 obstacks 部分 提供了 obstacks 编程接口。


    Apache Portable Runtime 文档 描述了它们的池式分配程序的接口。

    基本的分配程序
    Doug Lea 的 Malloc 是最流行的内存分配程序之一。


    BSD Malloc 用于大部分基于 BSD 的系统中。


    ptmalloc 起源于 Doug Lea 的 malloc,用于 GLIBC 之中。


    Hoard 是一个为多线程应用程序优化的 malloc 实现。


    GNU Memory-Mapped Malloc(GDB 的组成部分) 是一个基于 mmap() 的 malloc 实现。

    池式分配程序
    GNU Obstacks(GNU Libc 的组成部分)是安装最多的池式分配程序,因为在每一个基于 glibc 的系统中都有它。


    Apache 的池式分配程序(Apache Portable Runtime 中) 是应用最为广泛的池式分配程序。


    Squid 有其自己的池式分配程序。


    NetBSD 也有其自己的池式分配程序。


    talloc 是一个池式分配程序,是 Samba 的组成部分。

    智能指针和定制分配程序
    Loki C++ Library 有很多为 C++ 实现的通用模式,包括智能指针和一个定制的小对象分配程序。

    垃圾收集器
    Hahns Boehm Conservative Garbage Collector 是最流行的开源垃圾收集器,它可以用于常规的 C/C++ 程序。

    关于现代操作系统中的虚拟内存的文章
    Marshall Kirk McKusick 和 Michael J. Karels 合著的 A New Virtual Memory Implementation for Berkeley UNIX 讨论了 BSD 的 VM 系统。


    Mel Gorman's Linux VM Documentation 讨论了 Linux VM 系统。

    关于 malloc 的文章
    Poul-Henning Kamp 撰写的 Malloc in Modern Virtual Memory Environments 讨论的是 malloc 以及它如何与 BSD 虚拟内存交互。


    Berger、McKinley、Blumofe 和 Wilson 合著的 Hoard -- a Scalable Memory Allocator for Multithreaded Environments 讨论了 Hoard 分配程序的实现。


    Marshall Kirk McKusick 和 Michael J. Karels 合著的 Design of a General Purpose Memory Allocator for the 4.3BSD UNIX Kernel 讨论了内核级的分配程序。


    Doug Lea 撰写的 A Memory Allocator 给出了一个关于设计和实现分配程序的概述,其中包括设计选择与折衷。


    Emery D. Berger 撰写的 Memory Management for High-Performance Applications 讨论的是定制内存管理以及它如何影响高性能应用程序。

    关于定制分配程序的文章
    Doug Lea 撰写的 Some Storage Management Techniques for Container Classes 描述的是为 C++ 类编写定制分配程序。


    Berger、Zorn 和 McKinley 合著的 Composing High-Performance Memory Allocators 讨论了如何编写定制分配程序来加快具体工作的速度。


    Berger、Zorn 和 McKinley 合著的 Reconsidering Custom Memory Allocation 再次提及了定制分配的主题,看是否真正值得为其费心。

    关于垃圾收集的文章
    Paul R. Wilson 撰写的 Uniprocessor Garbage Collection Techniques 给出了垃圾收集的一个基本概述。


    Benjamin Zorn 撰写的 The Measured Cost of Garbage Collection 给出了关于垃圾收集和性能的硬数据(hard data)。


    Hans-Juergen Boehm 撰写的 Memory Allocation Myths and Half-Truths 给出了关于垃圾收集的神话(myths)。


    Hans-Juergen Boehm 撰写的 Space Efficient Conservative Garbage Collection 是一篇描述他的用于 C/C++ 的垃圾收集器的文章。

    Web 上的通用参考资料
    内存管理参考 中有很多关于内存管理参考资料和技术文章的链接。


    关于内存管理和内存层级的 OOPS Group Papers 是非常好的一组关于此主题的技术文章。


    C++ 中的内存管理讨论的是为 C++ 编写定制的分配程序。


    Programming Alternatives: Memory Management 讨论了程序员进行内存管理时的一些选择。


    垃圾收集 FAQ 讨论了关于垃圾收集您需要了解的所有内容。


    Richard Jones 的 Garbage Collection Bibliography 有指向任何您想要的关于垃圾收集的文章的链接。

    书籍
    Michael Daconta 撰写的 C++ Pointers and Dynamic Memory Management 介绍了关于内存管理的很多技术。


    Frantisek Franek 撰写的 Memory as a Programming Concept in C and C++ 讨论了有效使用内存的技术与工具,并给出了在计算机编程中应当引起注意的内存相关错误的角色。


    Richard Jones 和 Rafael Lins 合著的 Garbage Collection: Algorithms for Automatic Dynamic Memory Management 描述了当前使用的最常见的垃圾收集算法。


    在 Donald Knuth 撰写的 The Art of Computer Programming 第 1 卷 Fundamental Algorithms 的第 2.5 节“Dynamic Storage Allocation”中,描述了实现基本的分配程序的一些技术。


    在 Donald Knuth 撰写的 The Art of Computer Programming 第 1 卷 Fundamental Algorithms 的第 2.3.5 节“Lists and Garbage Collection”中,讨论了用于列表的垃圾收集算法。


    Andrei Alexandrescu 撰写的 Modern C++ Design 第 4 章“Small Object Allocation”描述了一个比 C++ 标准分配程序效率高得多的一个高速小对象分配程序。


    Andrei Alexandrescu 撰写的 Modern C++ Design 第 7 章“Smart Pointers”描述了在 C++ 中智能指针的实现。


    Jonathan 撰写的 Programming from the Ground Up 第 8 章“Intermediate Memory Topics”中有本文使用的简单分配程序的一个汇编语言版本。

    来自 developerWorks
    自我管理数据缓冲区内存 (developerWorks,2004 年 1 月)略述了一个用于管理内存的自管理的抽象数据缓存器的伪 C (pseudo-C)实现。


    A framework for the user defined malloc replacement feature (developerWorks,2002 年 2 月)展示了如何利用 AIX 中的一个工具,使用自己设计的内存子系统取代原有的内存子系统。


    掌握 Linux 调试技术 (developerWorks,2002 年 8 月)描述了可以使用调试方法的 4 种不同情形:段错误、内存溢出、内存泄漏和挂起。


    在 处理 Java 程序中的内存漏洞 (developerWorks,2001 年 2 月)中,了解导致 Java 内存泄漏的原因,以及何时需要考虑它们。


    在 developerWorks Linux 专区中,可以找到更多为 Linux 开发人员准备的参考资料。


    从 developerWorks 的 Speed-start your Linux app 专区中,可以下载运行于 Linux 之上的 IBM 中间件产品的免费测试版本,其中包括 WebSphere® Studio Application Developer、WebSphere Application Server、DB2® Universal Database、Tivoli® Access Manager 和 Tivoli Directory Server,查找 how-to 文章和技术支持。


    通过参与 developerWorks blogs 加入到 developerWorks 社区。


    可以在 Developer Bookstore Linux 专栏中定购 打折出售的 Linux 书籍。

    展开全文
  • 因此,如果对申请的一段存放数组的内存进行初始化,每个数组元素均初始化为特定的值,必须使用循环遍历来解决。 C++ Reference对memset函数的解释: void * memset ( void * ptr, int value, size_t num ); Fill...

    memset用于初始化数组,仅能初始化为0值,

    而不能初始化一个特定的值。

    因此,如果对申请的一段存放数组的内存进行初始化,每个数组元素均初始化为特定的值,必须使用循环遍历来解决。

    C++ Reference对memset函数的解释:

    void * memset ( void * ptr, int value, size_t num );

    Fill block of memory

    Sets the first num bytes of the block of memory pointed by ptr to the specified value (interpreted as an unsigned char).

    也就是说,value只能是unsigned char类型,num是Number of bytes to be set to the value.

    #include <bits/stdc++.h>
    using namespace std;
    int main ()
    {
      int str[10];
      memset (str,9,sizeof(str));
      for(int i=0;i<9;i++)
        cout<<str[i];
      return 0;
    }
    //output :151587081151587081151587081151587081151587081151587081151587081151587081151587081

     

    初始化字符

    #include <bits/stdc++.h>
    using namespace std;
    int main ()
    {
      char str[10];
      memset (str,'a',sizeof(str));
      str[9]='\0';
      puts(str);
      return 0;
    }
    //output :aaaaaaaaa

    复制代码

    展开全文
  • 日期 内核版本 架构 作者 GitHub CSDN 2016-06-14 ...在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换
  • 日期 内核版本 架构 作者 GitHub ... Linux内存管理在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到
  • 内存初始化(上)

    千次阅读 2016-10-13 20:34:27
    因为这时候内存管理模块还没有初始化,即便是memblock模块(初始化阶段分配内存的模块)都尚未初始化(没有内存布局的信息),不能动态分配内存。   五、early ioremap 除了DTB,在启动阶段,还有...
  • 外部内存初始化以及代码搬移 象棋小子 1048272975 对于处理器来说,都不可能内置过大的内存,只保留一小块SRAM作为芯片启动用。例如S3C2416内部SRAM只有64k,其中8k是作为SteppingStone,用来做一些基本的初始化...
  • Linux 内存管理系统:初始化

    千次阅读 2009-04-27 22:28:00
    作者:Joe Knapka臭翻:colyli内存管理系统的初始化处理流程分为三个基本阶段:激活页内存管理 在swapper_pg_dir中初始化内核的页表 初始化一系列和内存管理相关的内核数据 Turning On Paging (i386)启动分页...
  • 日期 内核版本 架构 作者 GitHub CSDN 2016-06-14 ...在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换
  • c++ : new 在特定指针处构造初始化

    千次阅读 2016-07-15 15:36:42
    C++ operator new 在特定指针处构造初始化我认为, 从安全和简化代码的角度考虑, 在编写C++代码时, 使用STL或其他模板库自动管理内存, 比通过new关键字或malloc动态声明空间, 更规范更安全, 带来的额外性能开销...
  • 日期 内核版本 架构 作者 GitHub ... Linux内存管理在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到
  • DPDK 代码分析一 : 内存初始化

    千次阅读 2016-06-05 13:18:45
    主进程只有一个,必须在从进程之前启动,负责执行DPDK库环境的初始化,从进程attach到主进程初始化的DPDK上,主进程先mmap hugetlbfs 文件,构建内存管理相关结构将这些结构存入hugetlbfs 上的配置文件rte_config,...
  • linux内存管理解析----linux物理,线性内存布局及页表的初始化 分类: linux内核2013-08-02 17:50 459人阅读 评论(0) 收藏 举报 linux内存管理页表初始化内存布局 主要议题: 1分页,分段模式及...
  • Linux节点和内存管理区的初始化

    千次阅读 2012-05-10 20:36:33
    节点和管理区是内存管理中所涉及的重要概念,其数据结构在前文... 在内核首先通过setup_arch()-->paging_init()-->zone_sizes_init()来初始化节点和管理区的一些数据项 static void __init zone_sizes_init(void) {
  • C++认识初始化

    千次阅读 2015-08-29 18:32:55
    初始化是程序设计中一项重要的...使用未初始化的变量(或内存区域)是程序产生bug的重要原因之一。正确理解和使用初始化操作,要弄清以下几个问题。1.什么是初始化在给初始化下定义前。先弄清楚两个概念。申明与定义。
  • Nginx源码分析(1)之——共享内存的配置、分配及初始化
  • eCos组件初始化

    千次阅读 2013-10-15 20:30:41
    mingdu.zheng <at> gmail <dot> comhttp://blog.csdn.net/zoomdy/article/...C++对象在初始化时不像C语言中的静态变量那样只是在特定内存单元写入特定的数值,C++对象在初始化时将会调用该对象类的...
  • JAVA之旅(三)——数组,堆栈内存结构,静态初始化,遍历,最值,选择/冒泡排序,二维数组,面向对象思想 我们继续JAVA之旅 一.数组1.概念 数组就是同一种类型数据的集合,就是一个容器数组的好处:可以自动给...
  • 结构体(声明、初始化内存对齐、如何传参)

    万次阅读 多人点赞 2018-03-08 19:31:10
    结构的初始化方式和数组初始化很相似。一个位于一对花括号内部,由逗号分隔的初始值列表可用于结构的初始化。这些值根据结构成员列表的顺序写出。 注:数组不可以整体赋值,可整体初始化。同理,结构体不可整体赋值...
  • C语言中,未初始化的局部变量到底是多少? 答案往往是: 与编译器有关。 可能但不保证初始化为0。 未确定。 总之,全部都是些一本正经的形而上答案,这很令人讨厌。 但凡一些人给你滔滔不绝地扯编译器,C库,...
  • DDR3内存详解,存储器结构+时序+初始化过程

    万次阅读 多人点赞 2017-06-17 16:10:33
    首先,我们先了解一下内存的大体结构工作流程,这样会比较容量理解这些参数在其中所起到的作用。这部分的讲述运用DDR3的简化时序图。  DDR3的内部是一个存储阵列,将数据“填”进去,你可以它想象成一张表格。和...
  • eCos组件初始化 .

    千次阅读 2013-10-21 11:21:24
    C++对象在初始化时不像C语言中的静态变量那样只是在特定内存单元写入特定的数值,C++对象在初始化时将会调用该对象类的构造函数来初始化对象。如果C++对象是在函数内声明,那么函数执行到对象的声明处调用类构造...
  • <!-- @page {margin:2cm} p {margin-bottom:0.21cm} --> 内存初始化是从全局启动例程start_kernel开始的。具体流程如下图: <!-- @page {margin:2cm}
  • DispatcherServlet类相关的结构...DispatcherServlet初始化了什么,可以在其initStrategies()方法中知晓,这个方法如下: protected void initStrategies(ApplicationContext context) { initMultipartResolver...
  • C++中对象初始化方式

    千次阅读 2019-03-18 23:34:28
    当对象在创建时获得了一个特定的值,我们说这个对象被初始化初始化不是赋值,初始化的含义是创建变量赋予其一个初始值,而赋值的含义是把当前值擦除,而以一个新值来替代。 对象初始化可以分为默认初始化、直接...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 312,470
精华内容 124,988
关键字:

后内存特定初始化