精华内容
下载资源
问答
  • 内存分配

    千次阅读 2013-01-24 10:58:25
    到目前为止,我们已经使用过kmalloc和kfree来分配秋释放内存,但Linux内核为我们提供了更加丰富的内存分配原语集。本章我们将介绍设备驱动程序中使用内存的一些其他方法。 kmalloc函数内幕 除非被阻塞,否则这...

    http://s.click.taobao.com/t?e=m%3D2%26s%3DBVM0kzNLmxocQipKwQzePDAVflQIoZepLKpWJ%2Bin0XJRAdhuF14FMULXpKGyEMu%2Flovu%2FCElQOvlaW6Pnw5RI9HO8a%2BfejaflyMnEnsOycKnA1HiGjbvTiI4FDKO59pQ

    到目前为止,我们已经使用过kmalloc和kfree来分配秋释放内存,但Linux内核为我们提供了更加丰富的内存分配原语集。本章我们将介绍设备驱动程序中使用内存的一些其他方法。

    kmalloc函数内幕

      1. 除非被阻塞,否则这个函数可运行得很快
      2. 不对所获取的内存空间清零,也就是分配给它的区域仍然保持着原有的数据
      3. 它分配的区域在物理内存中也是连续的

    flags参数

    记住kmalloc的原型是:

    #include <linux/slab.h>

    void *kmalloc(size_t size, int flags);

    kmalloc的第一个参数是要分配的块大小,第二个参数是分配标志,它能够以多种方式控制kmalloc的行为。

    最常用的标志是GFP_KERNEL,它表示内存分配(最终总是调用get_free_pages来实现的分配,这就是GFP_前缀的由来)是代表运行在内核空间的进程执行的。换句话说,这意味着调用它的函数正代表某个进程执行系统调用。使用GFP_KERNEL允许kmalloc在空闲内存较少的时候把当前进程转入休眠以等待一个页面。

    GFP_KERNEL分配标志并不始终适用,有时kmalloc是在进程上下文之外被调用的,例如在中断处理例程、tasklet以及内核定时器中调用。这种情况下驱动程序则应该换用GFP_ATOMIC标志。内核通常会为原子性的分配预留一些空闲页面。使用GFP_ATOMIC标志时,kmalloc甚至可以用掉最后一个空闲页面,如果最后一页都没有了,分配就返回失败。

    以上两个标志基本上可以满足大多数驱动程序的需要了。不过还有其它一些标志可用于替换和补充这两个标志。所有标志都定义在<linux/gfp.h>中,还有一些符号表示这些标志的常用组合。

    GFP_ATOMIC——用于在中断处理例程或其他运行于进程上下文之外的代码中分配内存,不会休眠。

    GFP_KERNEL——内核内存的通常分配方法,可能引起休眠。

    GFP_USER——用于为用户空间页分配内存,可能引起休眠。

    GFP_HIGHUSER——类似于GFP_USER,不过如果有高端内存的话就从那里分配。我们在下一小节讨论高端内存相关的话题。

    GFP_NOIO和GFP_NOFS——这两个标志的功能类似于GFP_KERNEL,但是为内核分配内存的工作方式添加了一些限制。具有GFP_NOFS标志的分配不允许执行任何文件系统调用,而GFP_NOIO禁止任何I/O的初始化。这两个标志主要在文件系统和虚拟内存代码中使用,这些代码中的内存分配可休眠,但不应该发生递归的文件系统调用。

    上面的标志可以和下面的标志“或”起来使用。下面这些标志控制如何进行分配:

    __GFP_DMA——该标志请求分配发生在可进行DMA的内存区段中。具体的含义是平台相关的,我们将在下一小节中解释。

    __GFP_HIGHMEM——这个标志表明要分配的内存可位于高端内存。

    __GFP_COLD——通常,内存分配器会试图返回“缓存热(cache warm)”页面,好可在处理器缓存中找到的页面。相反,这个标志请求尚未使用的“冷”页面。对用于DMA读取的页面分配,可使用这个标志,因为这种情况下,页面存在于处理器缓存中没有多大帮助。

    __GFP_NOWARN——该标志很少使用。它可以避免内核在无法满足分配请求时产生警告

    __GFP_HIGH——该标志标记了一个高优先级的请求,它允许为紧急状况而消耗由内核保留的最后一些页面。

    __GFP_REPEAT、__GFP_NOFAIL、__GFP_NORETRY——这些标志告诉分配器在满足分配请求而遇到困难时应该采取体积行为。__GFP_REPEAT表示努力再尝试一次,它会重新尝试分配,但仍有可能失败。__GFP_NOFAIL标志告诉分配器始终不返回失败,它会努力满足分配请求。我们不鼓励使用__GFP_NOFAIL标志,因为在设备驱动程序中,从没有理由需要使用这个标志。最后,__GFP_NORETRY告诉分配器,如果所请求的内存不可获得,就立即返回。

    内存区段

    __GFP_DMA和__GFP_HIGHMEM的使用与平台相关,尽管在所有平台上都可以使用这两个标志。

    Linux内核把内存分为三个区段:可用于DMA的内存、常规内存以及高端内存。通常的内存分配都发生在常规内存区。但通过设置上面介绍的标志也可请求在其他区段中分配。如果指定了__GFP_DMA标志,则只有DMA区段会被搜索:如果低地址段上没有可用内存,分配就会失败。如果没有指定特定标志,则常规区和DMA区段都会被搜索;而如果设置了__GFP_HIGHMEM标志,则所有三个区段都会被搜索以获取一个空闲页(注意,kmalloc不能分配高端内存)

    内存区段的背后机制在mm/page_alloc.c中实现,区段的初始化是平台相关的,通常在对应的arch树下的mm/init.c中。第十五章还会再次讨论这个问题。

    size参数

    内核负责管理系统物理内存,物理内存只能按页面进行分配。因简单的基于堆的内存分配技术在处理页面边界很棘手,所以内核使用了特殊的基于页的分配技术,以最佳地利用系统RAM。

    Linux处理内存分配的方法是,创建一系列的内存对象池,每个池中的内存块大小是固定一致的。处理分配请求时,就直接在包含有足够大的内存块的池中传递一个整块给请求者。

    驱动程序开发者应该记住一点,就是内核只能分配一些预定义的,固定大小的字节数组。如果申请任意数量的内存,那么得到的很可能会多一些,最多会到申请数量的两倍。别处,程序员应该记住,kmalloc能处理的最小的内存块是32或者64,到底是哪个则取决于当前体系结构使用的页面大小。

    对kmalloc能够分配的内存快大小,存在一个上限。这个限制随着系统架构的不同以及内核配置选项的不同而变化。如果我们希望代码具有完整的可移植性,则不应该分配大于128KB的内存。但是如果希望得到多于几千字节的内存,则最好使用除kmalloc之外的内存获取方法。我们将在稍后描述这些方法。

    后备高速缓存

     设备驱动程序常常会反复地分配很多同一大小的内存块。既然内核已经维护了一组拥有同一大小内存块的内存池,那为什么不为这些反复使用的块增加某些特殊的内存池呢?实际上,内核的确实现了这种形式的内存池,通常称为后备高速缓存(lookaside cache)。设备驱动程序通常不会涉及这种使用后备高速缓存的内存行为,但也有例外,Linux 2.6中的USB和SCSI驱动程序就使用了这种高速缓存。

    Linux内核的高速缓存管理有时称为“slab分配器”。相关函数在<linux/slab.h>中声明。slab分配器实现的高速缓存具有kmem_cache_t类型,可通过调用kmem_cache_create创建:
    kmem_cache_t *kmem_cache_create(const char *name, size_t size,
                                    size_t offset,
                                    unsigned long flags,
                                    void(*constructor)(void *, kmem_cache_t *,
                                                       unsigned long flags),
                                    void(*destructor)(void *, kmem_cache_t *,
                                                       unsigned long flags));

    该函数创建一个新的高速缓存对象,其中可以容纳任意数目的内存区域,这些区域的大小都相同,由size参数指定。参数name与这个高速缓存相关联,其功能是保管一些信息以便追踪问题,它通常设置为将要高速缓存的结构的名字。高速缓存保留指向该名称的指针,而不是复制内容,因此,驱动程序应该将指向静态存储(通常可取直接字符串)的指针传递给这个函数。名称中不能包含空白。

    offset参数是页面中的第一个对象的偏移量,它可以用来确保对已分配的对象进行某种特殊的对齐,但是最常用的就是0,表示默认值。flags控制如何完成分配,是一个位掩码,可取的值如下:

    SLAB_NO_REAP——设置这个标志可以保护高速缓存在系统寻找内存的时候不会被减少。设置该标志通常不是好主意,因为我们不应该对内存分配器的自由做一些人为的、不必要的限制。

    SLAB_HWCACHE_ALIGN——这个标志要求所有数据对象跟高速缓存行对齐;实际的操作则依赖于主机平台的硬件高速缓存布局。如果在SMP机器上,高速缓存中包含有频繁访问的数据项的话,则该标志将是非常好的选择。但是,为了满足高速缓存行的对齐需求,必要的填白可能浪费大量的内存。

    SLAB_CACHE_DMA——这个标志要求每个数据对象都从可用于DMA的内存区段中分配。

    还有一些标志可用于高速缓存分配的调度,详情请见mm/slab.c文件。但通常这些标志只在开发系统中通过内核配置选项而全局的设置。

    constructor和destructor参数是可选参数(但是不能只有des而没有con);前者用于初始化新分配的对象,而后者用于“清除”对象——在内存空间被整个释放给系统之前。

    一旦某个对象的高速缓存被创建,就可以调用kmem_cache_alloc从中分配内存对象:

    void *kmem_cache_alloc(kmem_cache_t *cache, int flags);

    参数cache是先前创建的高速缓存;参数flags和传递给kmalloc的相同,当需要分配更多的内存来满足kmem_cache_alloc时,高速缓存还会利用这个参数。

    释放一个内存对象时使用kmem_cache_free:

    void kmem_cache_free(kmem_cache_t *cache, const void *obj);

    如果驱动程序代码中和高速缓存有关的部分已经处理完了(一个典型情况是模块被卸载的时候),这时驱动程序应该释放它的高速缓存,如下所示:

    int kmem_cache_destroy(kmem_cache_t *cache);

    这个释放操作只有在已将从缓存中分配的所有对象都归还后才能成功。所以,模块应该检查kmem_cache_destroy返回状态;如果失败,则表明模块中发生了内存泄漏(因为有一些对象被漏掉了)。

    使用后备式缓存带来的另一个好处是内核可以统计高速缓存的使用情况。高速缓存的使用统计情况可以从/proc/slabinfo获得。

    基于slab高速缓存的scull:scullc

    与scull使用kmalloc不同的是,scullc使用内存高速缓存。数据对象大小可以编译或加载时修改,但是不能在运行时修改——那样需要创建一个新的高速缓存,而这里不必处理那些不必要的细节问题。

    scullc可以用于测试slab分配器。它和scull只有几行代码不同。我们必须首先声明自己的slab高速缓存:
    /*声明一个高速缓存指针,它将用于所有的设备*/
    kmem_cache_t *scullc_cache;

    slab高速缓存的创建代码如下所示(在模块装载阶段):

    /*scullc_init:为我们的量子创建一个高速缓存*/
    scullc_cache = kmem_cache_create("scullc", scullc_quantum,
            0, SLAB_HWCACHE_ALIGN, NULL, NULL);/*没有ctor和dtor*/
    if(!scullc_cache) {
        scullc_cleanup();
        return -ENOMEM;
    }

    下面是分配内存量子的代码:

    /*使用内存高速缓存来分配一个量子*/
    if(!dptr->data[s_pos]) {
        dptr->data[s_pos] = kmem_cache_alloc(scullc_cache, GFP_KERNEL);
        if(!dptr->data[s_pos])
            goto nomem;
        memset(dptr->data[s_pos], 0, scullc_quantum);
    }

    下面的代码将释放内存:

    for(i = 0; i < qset; i++)
    if(dptr->data[i])
        kmem_cache_free(scullc_cache, dptr->data[i]);

    最后,在模块卸载期间,我们必须将高速缓存返回给系统:
    /*scullc_cleanup:释放量子使用的高速缓存*/
    if(scullc_cache)
        kmem_cache_destroy(scullc_cache);

    和scull相比,scullc的最主要差别是运行速度略有所提高,并且对内存的利用率更佳。由于数据对象是从内存池中分配的,而内存池中的所有内存块大小都具有同样的大小,所以这些数据在内存中的位置排列达到了最大程度的密集,相反的,scull的数据对象则会引入不可预测的内存碎片。

    内存池

    内核中有些地方的内存分配是不允许失败的。为了确保这种情况下的成功分配,内核开发者建立了一种称为内存池(或者“mempoll")的抽象。内存池其实就是某种形式的后备高速缓存,它试图始终保存空闲的内存,以便在紧急状态下使用。

    内存池对象的类型为mempool_t(在<linux/mempool.h>中定义),可使用mempool_create来建立内存池对象:

    mempool_t *mempool_create(int min_nr,
                              mempool_alloc_t *alloc_fn,
                              mempool_free_t *free_fn,
                              void *pool_data);  

    min_nr参数表示的是内存池应该始终保持的已分配对象的最少数目。对象的实际分配和释放由alloc_fn和free_fn函数处理,其原型如下:

    typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);
    typedef void (mempool_free_t)(void *element, void *pool_data);

    mempool_create的最后一个参数,即pool_data,被传入alloc_fn和free_fn。

    如有必要,我们可以为mempool编写特定用途的函数来处理内存分配。但是,通常我们仅会让内核的slab分配器为我们处理这个任务。内核中有两个函数(mempool_alloc_slab和mempool_free_slab),它们的原型和上述内存池分配原型匹配,并利用kmem_cache_alloc和kmem_cache_free处理内存分配和释放。因此,构造内存池的代码通常如下所示:

    cache = kmem_cache_create(...);
    pool = mempool_create(MY_POOL_MINIMUM,
                          mem_pool_alloc_slab, mempool_free_slab,
                          cache);

    在建立内存池之后,可如下所示分配和释放对象:

    void *mempool_alloc(mempool_t *pool, int gfp_mask);
    void mempool_free(void *element, mempool_t *pool);

    在创建mempool时,就会多次调用分配函数为预先分配的对象创建内存池。之后,对mempool_alloc的调用将首先通过分配函数获得该对象;如果该分配失败,就会返回预先分配的对象(如果存在的话)。如果使用mempool_free释放一个对象,则如果预先分配的对象数目小于要求的最低数目(min_nr),就会将该对象保留在内存池中;否则,该对象会返回给系统。

    我们可以利用下面的函数来调整mempool的大小:

    int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);//gfp_mask同kmalloc的标志一样

    如果对该函数调用成功,将反内存池的大小调整为至少有new_min_nr个预分配对象。

    如果不再需要内存池,可使用下面的函数将其返回给系统:

    void mempool_destroy(mempool_t *pool);

    在销毁mempool之前,必须将所有已分配的对象返回到内存池中,否则会导致内核oops。

    如果读者计划在自己的驱动程序中使用mempool,则应该记住下面这点:mempool会分配一些内存块,空闲且不会真正得到使用。因此,使用mempool很容易浪费大量内存。几乎在所有情况下,最好不要使用mempool而是处理可能的分配失败。如果驱动程序存在某种方式可以响应分配的失败,而不会导致对系统一致性的破坏,则应该使用这种方式,也就是说,应尽量避免在驱动程序代码中使用mempool。

    get_free_page和相关函数

     如果模块要分配大块的内存,使用面向页的分配技术会更好些。整页的分配还有其他优点,以后会在第十五章介绍。

    分配页面可使用正面的函数:

    get_zeroed_page(unsigned int flags);//返回指向新页面的指针并将页面清零。

     __get_free_page(unsigned int flags); //类似于get_zeroed_page,但不清零页面。

    __get_free_pages(unsigned int flags, unsigned int order);//分配若干(物理连续的)页面,并返回 指向该内存区域第一个字节的指针,但不清零页面。

    参数flags的作用和kmalloc中的一样;通常使用GFP_KERNEL或GFP_ATOMIC,也许还会加上__GFP_DMA标志(申请可用于ISA直接内存访问操作的内存)或者__GFP_HIGHMEM标志(使用高端内存)。参数order是要申请或释放的页面数的以2为底的对数。例如为0表示一个页面,为3表示8个页面。如果order太大,而又没有那么大的连续区域可以分配,就会返回失败。get_order函数返回order值。可允许的最大order值是10或者11(对应于1024或2048个页),这依赖于体系结构。

    如果读者对此好奇,/proc/buddyinfo可告诉你系统中每个内存区段上每个除数下可获得的数据块数目。

    当程序不再需要使用页面时,它可以通过使用下列函数之一来释放它们。第一个函数是一个宏,展开后就是对第二个函数的调用:

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

    如果试图释放和先前分配数目不等的页面,内存映射关系就会被破坏,随后系统就会出错。

    值得强调的是,只要符合和kmalloc同样的规则,get_free_pages和其他函数可以在任何时间调用。某些情况下函数分配内存会失败,特别是在使用了GFP_ATOMIC的时候。因此,调用了这些函数的程序在分配出错时都应该提供相应的处理。

    尽管kmalloc(GFP_KERNEL)在没有空闲内存时有时会返回失败,但内核总会尽可能满足它。因此,如果分配太多内存,系统的响应性能就很容易降下来。例如往scull设备写入大量数据,计算机很可能就会死掉;当系统为满足kmalloc分配请求而试图掏出尽可能多的内存页时,就会变得很慢。所有资源都被贪婪的设备所吞噬,计算机很快就会变得无法使用了;此时甚至已经无法为解决这个问题而生成新的进程。我们没有在scull中提到这个问题是因为它是个例子,并不会真正在多用户系统中使用。但作为一个编程者必须要小心,因为模块是特权代码,会带来新的系统安全漏洞,例如很可能会造成DoS(denail-of-service,拒绝服务攻击)安全漏洞。

    使用整页的scull:scullp(实际测试页面分配)

    scullp分配的内存数量是一个或数个整页:scullp_order变量默认为0,但可以在编译或加载时更改。

    下列代码说明了它如何分配内存:

    /*下面分配单个量子*/
    if(!dptr->data[s_pos]){
        dptr->data[s_pos] =
            (void *)__get_free_pages(GFP_KERNEL, dptr->order);
        if(!dptr->data[s_pos])
            goto nomem;
        memset(dptr->data[s_pos], 0, PAGE_SIZE << dptr->order);
    }

    scullp中释放内存的代码如下:

    for(i = 0; i < qset; i++)
        if(dptr->data[i])
            free_pages((unsigned long)(dptr->data[i]),
                dptr->order);

    从用户角度来看,可以感觉到的差别就是速度快了一些,并且内存利用率更高了,因为不会有内存碎片。但性能提高并不多,因为kmalloc已经运行得很快。基于页的分配策略的优点实际不在速度上,而在于更有效地使用了内存。按页分配不会浪费内存空间,而用kmalloc函数则会因分配粒度的原因而浪费一定数量的内存。

    使用__get_free_page函数的最大优点是这些分配的页面完全属于我们自己,而且在理论上可以通过适当地调整页表将它们合并成一个线性区域。例如,可以允许用户进程对这些单一的并互不相关的页面分配得到的内存区域进行mmap。

    alloc_pages接口

     为完整起见,本节将介绍内存分配的另一个接口,但是在十五章才会使用这个接口。现在,我们只要知道struct page是内核用来描述单个内存页的数据结构就足够了。我们将看到,在内核中有许多地方要使用page结构,尤其在需要使用高端内存(高端内存在内核中没有对应不变的地址)的地方。

    Linux页分配器的核心代码是称为alloc_pages_node的函数:

    struct page *alloc_pages_node(int nid, unsigned int flags, unsigned int order);

    这个函数具有两个变种(它们只是简单的宏),大多数情况下我们使用这两个宏:

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

    struct page *alloc_page(unsigned int flags);

    核心函数alloc_pages_node要求传入三人参数。nid是NUMA节点的ID号,表示要在其中分配内存,flags是通常的GFP_分配标志,而order是要分配的内存大小。该函数返回指向第一个page结构(可能返回多个页)的指针,它描述了已分配的内存;或者在失败时返回NULL。

    alloc_pages通过在当前的NUMA节点上分配内存而简化了alloc_pages_node函数,它将numa_node_id的返回值作为nid参数而调用了alloc_pages_node函数。另外,alloc_page函数显然忽略了order参数而只分配单人页面。

    为了释放通过上述途径分配的页面,我们应该使用正面的函数:

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

    如果知道某个页面中的内容是否驻留在处理器调整缓存中,则应该使用free_hot_page(用于驻留在调整缓存中的页)或者free_cold_page和内核通信。这个信息可帮助内存分配器优化内存的使用。

    vmalloc及其辅助函数

    下面要介绍的内存分配函数是vmalloc,它分配虚拟地址空间的连续区域。尽管这些区域在物理上可能不是连续的(要访问其中的每个页面都必须独立地调用函数alloc_page),内核却认为它们是连续的。vmalloc在发生错误时返回0(NULL地址),成功时返回一个指针,该指针指向一个线性的、大小最少为size的线性内存区域。

    我们在这里描述vmalloc的原因是,它是Linux内存分配机制的基础。但是,我们要注意在大多数情况下不鼓励使用vmalloc。通过vmalloc获得的内存使用起来效率不高,而且在某些体系架构上,用于vmalloc的地址空间总量相对较小。如果希望将使用vmalloc的代码提交给内核主线代码,则可能会受到冷遇。如果可能,应该直接和单个页面打交道,而不是使用vmalloc。

    虽然这么说我们还是来看看如何使用vmalloc。该函数的原型及其相关函数(ioremap,并不是严格的分配函数,将在本节后面讨论)如下所示:

    void *vmalloc(unsigned long size);
    void vfree(void *addr);
    void *ioremap(unsigned long offset, unsigned long size);
    void iounmap(void *addr);

    要强调的是,由kmalloc和__get_free_pages返回的内存地址仍然是虚拟地址,其实际值仍然要由MMU(内存管理单元,通常是CPU的组成部分)处理才能转为物理内存地址(注:有些体系结构保留虚拟地址范围,用于寻址物理内存)。vmalloc在如何使用硬件上没有区别,区别在于内核如何执行分配任务。

    kmalloc和__get_free_pages使用的(虚拟)地址范围与物理内存是一一对应的,可能会有基于常量PAGE_OFFSET的一个偏移。这两个函数不需要为该地址段修改页表。但是,vmalloc和ioremap使用的地址范围完全是虚拟的,所以每次分配都通过对页表的适当设置来建立(虚拟)内存区域。

    可以通过比较内存分配函数返回的指针来发现这种差别。某些平台上(如x86),vmalloc返回的地址仅仅比kmalloc返回的地址高一些;而其它平台上(如MIPS和IA-64),它们就完全属于不同的地址范围了。vmalloc可以获得的地址范围在VMALLOC_START到VMALLOC_END的范围中。这两个符号都在<asm/pgtable.h>中定义。

    用vmalloc分配得到的地址是不能在微处理器这外使用的,因为它们只在处理器的内存管理单元上才有意义。当驱动程序需要真正的物理地址时(像外设用驱动系统总线的DMA地址),就不能使用vmalloc了。使用vmalloc函数的正确场合是在分配一大块连续的、只在软件中才存在的、用于缓冲的内存区域的时候。注意vmalloc的开销要比__get_free_pages大,因为它不但获取内存,还要建立页表。因此,用vmalloc函数分配仅仅一页的内存空间是不值得的。

    使用vmalloc函数牟一个例子函数是create_module系统调用,它利用vmalloc函数来获取装载模块所需的内存空间。在调用insmod来重定位模块代码后,接着会调用copy_frmo_user函数把模块代码和数据复制到分配而得的空间内。这样模块看来像是在连续的内存空间内。但通过检查/proc/ksyms文件就能发现模块导出的内核符号和内核本身导出的符号分布在不同的内存范围上。

    用vmalloc分配得到的空间要用vfree函数来释放。

    和vmalloc一样,ioremap也建立新的页表,但和vmalloc不同的是,ioremap并不实际分配内存。ioremap的返回值是一个特殊的虚拟地址,可以用来访问指定的物理内存区域,这个虚拟地址最后要调用iounmap来释放掉。

    ioremap更多用于(物理的)PCI缓冲区地址到(虚拟的)内核空间。例如,可以用来访问PCI视频设备的帧缓冲区;该缓冲区通常被映射到高物理地址,超出了系统初始化时建立的页表地址范围。

    要注意,为了保持可移植性,不应把ioremap返回的地址当作指向内存的指针而直接访问。相反,应该使用readb或者其他I/O函数,这是因为,在如Alpha的一些平台上,由于PCI规范和Alpha处理器在数据 传输方式上的差异,不能直接把PCI内存区域映射到处理器的地址空间。

    ioremap和vmalloc函数都是面向页的(它们都会修改页表),因此重新定位或分配的内存空间实际上都会上调到最近的一个页边界。ioremap通过把重新映射的地址向下下调到页边界,并返回在第一个重新映射页面中的偏移量的方法模拟了不对齐的映射。

    vmalloc函数的一个小缺点是它不能在原子上下文中使用,因为它内部实现调用了kmalloc(GFP_KERNEL)来获取页表的存储空间,因而可能休眠。但这不是什么问题,因为如果__get_free_page函数都还不能满足中断处理例程的需求的话,那应该修改软件的设计了。

    使用虚拟地址的scull:scullv

    scullv模块使用了vmalloc。和scullp一样,这个模块也是scull的一人缩减版本,只是使用了不同的分配函数来获取内存空间。

    该模块每次分配16页的内存。这里的内存分配使用了较大的数据块以获取比scullp更好的性能,并且展示了为什么使用其他分配技术会更耗时。用__get_free_pages函数来分配一页以上的内存空间容易出错,而且即使成功了也会比较慢。在前面我们已经看到,用vmalloc分配几个页时比其他函数要快一些,但由于存在建立页表的开销,所不当只分配一页时会慢一些。scullv设计得和scullp很相似。order参数指定每次要分配的内存空间的“阶数”,默认为4。scullv和scullp的唯一差别是在分配管理上。下面的代码用vmalloc获取新内存:

    /*使用虚拟地址分配一个量子*/
    if(!dptr->data[s_pos]){
        dptr->data[s_pos] =
            (void *)vmalloc(PAGE_SIZE << dptr->order);
        if(!dptr->data[s_pos])
            goto nomem;
        memset(dptr->data[s_pos], 0, PAGE_SIZE << dptr->order);
    }

    scullv中释放内存的代码如下:

    for(i = 0; i < qset; i++)
        if(dptr->data[i])
            vfree(dptr->data[i]);

    per-CPU变量

    per-CPU(每CPU)变量是2.6内核的一个有趣的特性。当建立一个per-CPU变量时,系统中的每个处理器都会拥有该变量的特有副本。这看起来有些奇怪,但它有其优点。对per-CPU变量的访问(几乎)不需要锁定,因为每个处理器在其自己的副本上工作。per-CPU变量还可以保存在对应处理器的调整缓存中,这样,就可以在频繁更新时获得更好的性能。

    关于per-CPU变量使用的例子可见于网线子系统中。内核维护着大量计数器,这些计数器跟踪已接收到的各类数据包数量,而这些计数器每秒可能被更新上千次。网络子系统的开发者将这些统计用的计数器放在了per-CPU变量中,这样,他们就不需要处理缓存和锁定问题,而更新可在不用锁定的情况下快速完成。在用户空间偶尔请求这些计数器的值时,只需要将每个处理器的版本相加并返回合计值即可。

    关于per-CPU变量的声明可见于<linux/percpu.h>中。要在编译期间创建一个per-CPU变量,可使用下面的宏:

    DEFINE_PER_CPU(type, name);

    如果该变量是一个数组,需要在type中包含数组的维数。这样,具有三个整数的per-CPU数组变量可通过下面的语句建立:

    DEFINE_PER_CPU(int[3], my_percpu_array);

    对per-CPU变量的操作几乎不使用任何锁定即可完成。但要记得2.6内核是抢占式的;也就是说,当处理器在修改某个per-CPU变量的临界区中间,可能会被抢占,因此应该避免这种情况的发生。我们还应该避免进程正在访问一个per-CPU变量时被切换到另一个处理器上运行。为此我们应该显式地调用get_cpu_var宏访问某给定变量的当前处理器副本,结束后调用put_cpu_var。对get_cpu_var的调用将返回当前处理器变量版本的1value值,并禁止抢占。因为返回的是1value,因此可直接赋值或者操作。例如,网络代码对一个计数器的递增使用了下面的两条语句:

    get_cpu_var(sockets_in_use)++;
    put_cpu_var(sockets_in_use);

    我们还可以使用下面的宏访问其他处理器的变量副本:

    per_cpu(variable, int cpu_id);

    如果我们要编写的代码涉及到多个处理器的per-CPU变量,这时则需要采用某种锁定机制来确保访问安全。

    动态分配per-CPU变量也是可能的。这时,应用下面的函数分配变量:

    void *alloc_percpu(type);
    void *__alloc_percpu(size_t size, size_t align);

    在大多数情况下可使用alloc_percpu完成分配工作;但如果需要特定的对齐,则应该调用__alloc_percpu函数。不管使用哪个函数,可使用free_percpu将per-CPU变量返回给系统。对动态分配的per-CPU变量的访问通过per_cpu_ptr完成:

    per_cpu_ptr(void *per_cpu_var, int cpu_id);

    这人宏返回指向对应于给定cpu_id的per_cpu_var版本的指针。如果打算读取该变量的其他CPU版本,则可以引用该指针进行相关操作。但是,如果正在操作当前处理器的版本,则应该首先确保自己不会被切换到其他处理器上运行。如果对per-CPU的整个访问发生在拥有某个自旋锁的情况下,则不会出现任何问题。但是,在使用该变量的时候通常需要使用get_cpu来阻塞抢占。这样使用动态per-CPU变量的代码类似下面所示:

    int cpu;

    cpu = get_cpu();
    ptr = per_cpu_ptr(per_cpu_var, cpu);
    /*使用ptr*/
    put_cpu();

    per-CPU变量可以导出给模块,但是必须使用下面的宏:

    EXPORT_PER_CPU_SYMBOL(per_cpu_var);
    EXPORT_PER_CPU_SYMBOL_GPL(per_cpu_var);

    要在模块中访问这样一个变量,则应该将其声明如下:

    使用DECLARE_PER_CPU(而不是DEFINE_PER_CPU),将告诉编译器要使用一个外部引用。

    如果打算使用per-CPU变量来建立简单的整数计数器,可参考<linux/percpu_counter.h>中已封装好的实现。最后要注意,在某些体系架构上,per-CPU变量可使用的地址空间是受限制的。因此,如果要创建per-CPU变量,则应该保持这些变量较小。

    获取大的缓冲区

    我们在前面的小节中提到,大的、连续内存缓冲区的分配易流于失败。系统内存会随着时间的流逝而碎片化,这导致无法获得真正的大内存区域。因为可以有其它途径来完成工作而不需要大的缓冲区,所以内核开发者并没有将大缓冲区的分配工作作为高优先级的任务来计划。在试图获得大的缓冲区前我们应该仔细考虑是否有其他的实现途径。到目前为止,执行大的I/O操作的最好方式是通过离散/聚集操作。我们将在第十章中讨论这各操作。

    在引导时获得专用缓冲区

    如果的确需要连续的大块内存用作缓冲区,就最好在系统引导期间通过请求内存来分配。在引导时就进行分配是获得大量连续内存页面的唯一方法,它绕过了__get_free_jpages函数在缓冲区大小上的最大尺寸和固定粒度的双重限制。在引导时分配缓冲区有点“脏”,因为通过保留私有内存池而跳过了内核的内存管理策略。这种技术比较粗暴也很不灵活,但也是最不容易失败的。显然,模块不能在引导时分配内存,而只有直接链接到内核的设备驱动程序才能在引导时分配内存。

    还有一个值得注意的问题是,对于变通用户来说引导时分配不是一个切实可用的的选项,因为这种机制只对链接到内核映像中的代码可用。要安装或替换使用了这种分配技术的驱动程序,就只能重新编译内核并重新启动计算机。

    内核被引导时,它可访问系统所有的物理内存,然后调用各个子系统的初始化函数进行初始化,它允许初始化代码分配私有的缓冲区,同时减少了留给系统操作的RAM数量。

    通过调用下列函数之一则可完成引导时的内存分配:

    #include <linux/bootmem.h>
    void *alloc_bootmem(unsigned long size);
    void *alloc_bootmem_low(unsigned long size);
    void *alloc_bootmem_pages(unsigned long size);
    void *alloc_bootmem_low_pages(unsigned long size);

    这些函数要么分配整个页(若以_pages结尾),要么分配不在页边界上对齐的内存区。

    除非使用具有_low后缀的版本,否则分配的内存可能会是高端内存。如果我们正在为设备驱动程序分配缓冲区,则可能希望将其用于DMA操作,而高端内存并不总是支持DMA操作;这样,我们可能需要使用上述函数的一个_low变种。

    很少会释放引导时分配的内存,而且也没有任何办法可将这些内存再次拿回。但是内核还是提供了一种释这种内存的接口:

    void free_bootmem(unsigned long addr, unsigned  long size);

    注意,通过上述函数释放的部分页面不会返回给系统——但是,如果我们使用这种技术,则其实已经分配得到了一些完整的页面。

    如果必须使用引导时的分配,则应该将驱动程序直接链接到内核。关于直接链接到内核的实现细节,可参阅内核源代码中Documentation/kbuild目录下的文件。

    
    展开全文
  • 静态内存分配与动态内存分配

    千次阅读 2019-05-25 14:05:02
    静态内存分配与动态内存分配 动机 平时看c/c++的书籍时,总会看到一种观点,说是C/C++语言使用的时候动态内存分配是最重要的,使用malloc等函数分配的内存必须要释放,否则及其容易出现内存泄露。但是自己有时候挺...

    静态内存分配与动态内存分配

    动机

    平时看c/c++的书籍时,总会看到一种观点,说是C/C++语言使用的时候动态内存分配是最重要的,使用malloc等函数分配的内存必须要释放,否则及其容易出现内存泄露。但是自己有时候挺奇怪的,啥时候的内存要自己释放,啥时候的内存是编译器释放呢?

    内存模型

    在这里插入图片描述
    这是csapp上面的linux进程内存分布图,从图中最下面往上看,只读段和读写段,在代码编译结束的时候就已经固定了,这一部分在整个进程生命周期是不会改变的。
    这一部分的内存是动态分配的,也就是需要我们程序员自己进行释放的,
    共享库这是链接的时候复制过来的库的代码 所以其实也是固定的
    ==栈-==用户站,编译器用它来实现函数调用,它是动态的,但是在这一部分,也就是函数内部的变量,是不需要程序员手动管理的,由编译器管理。

    通过对比加深理解

    内存的静态分配和动态分配的区别主要是两个:

    一是时间不同。静态分配发生在程序编译和连接的时候。动态分配则发生在程序调入和执行的时候。
    二是空间不同堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由函数malloc进行分配。不过栈的动态分配和堆不同,他的动态分配是由编译器进行释放,无需我们手工实现。

    对于一个进程的内存空间而言,可以在逻辑上分成3个部份:代码区,静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈(stack)”和“堆(heap)”是两种不同的动态数据区,栈是一种线性结构,堆是一种链式结构。进程的每个线程都有私有的“栈”,所以每个线程虽然代码一样,但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量。

    所以,举个生动的例子就是说,静态分配内存的数据都是从生下来就拥有内存位置,不需要你去管理,而动态内存是后天要拼搏获取的内存位置,同时用完了也要释放

    动态内存分配是哪些数据?

    1.变长数组。
    2.声明的指针指向一个内存,用malloc等内存分配函数申请。

    总结

    最后发现就是一句话,如果有自己申请的就需要自己释放,不然就没有什么关系,而自己申请的大多也都是声明一个指针然后调用内存申请的函数,现代C++都是用RAII就没有这个问题了,析构的时候一般就不需要手动释放内存了。

    展开全文
  • 内存分配方式与内存分配算法

    千次阅读 2018-03-14 20:24:56
    内存分配方式有两种,连续内存分配方式和离散内存分配方式。不同的分配方式又有不同的分配算法。 内存分配算法,其实就是:有一大块空闲的资源,如何合理地分配资源?内存分配的思想可以用到很多其他的领域。比如...

    内存分配方式有两种,连续内存分配方式和离散内存分配方式。不同的分配方式又有不同的分配算法。

    内存分配算法,其实就是:有一大块空闲的资源,如何合理地分配资源?内存分配的思想可以用到很多其他的领域。比如Java虚拟机是如何将内存分配与回收的?再比如文件系统是如何将磁盘块分配与回收的?其本质就是如何把空闲的资源分配出去,分配之后又如何回收?目标就是分配快,回收也快,而且还不浪费。那么,就需要根据资源的特点、以及应用场景做权衡从而选择何种方式进行分配与回收。

    ①连续内存分配方式

    1)固定分区分配

    将内存划分成若干个固定大小的块。将程序装入块中即可。内存划分成各个块之后,块大小不再改变。当然,划分块的方式有:所有的块大小相等;划分的块大小不相等。

    这种方式,在实际的内存分配之前,就已经知道了所有的内存块大小了。

    2)动态分区分配

    需要一个空闲表 或者 空闲链 来记录目前系统中空间的内存区域。在内存分配时,需要查找空间表或空闲链找到一块内存分配给当前进程。

    动态分区分配算法:

    a)首次适应法

    b)循环首次适应法

    c)最佳适应法

    d)最坏适应法

    e)快速适应法

    3)可重定位分区分配

    说白了,就是增加了内存移动的功能。由于若干次内存分配与回收之后,各个空闲的内存块不连续了。通过“重定位”,将已经分配的内存“紧凑”在一块(就类似于JVM垃圾回收中的复制算法)从而空出一大块空闲的内存出来。

    ”紧凑“是需要开销的,比如需要重新计算 地址,这也为什么JVM垃圾回收会导致STW的原因。

    而离散分配方式–不管是分页还是分段,都是直接将程序放到各个离散的页中。从而就不存在“紧凑”一说了。

    连续内存分配方式涉及两种操作:内存分配操作 和 内存回收操作

    ②离散内存分配方式

    内存资源是有限的,程序要运行,必须得加载到内存。如果内存已经满了,而现在又有新的程序要运行,怎么办?—SWAP

    把当前不用的程序(数据)先换出内存,从而就有空间 加载当前需要运行的程序的一部分数据进入内存,这样大大提高了内存的利用率。

    由于牵涉到换入与换出,前面的连续内存分配方式就有点不适用了。因为,最明显的一个问题:对于连续内存分配方式,究竟换出哪部分数据呢?

    而这种只装入部分”数据”就可以使程序运行的机制,就是虚拟存储器的本质。

    1)分页存储管理

    将进程的逻辑地址空间分成若干大小相等的页面;同时,也将物理内存分成相等大小的页面(称为块或frame)。在为进程分配内存时,以块为单位将进程的若干页 可以 装入到内存中多个不邻接的物理块中。

    从上可以看出:“离散” 体现在:进程在内存中分配的空间(物理块)是不连续的。而对于连续分配方式,进程在内存的分配的空间是连续的。

    现在考虑32位系统,每个物理块的大小为4KB。如何把逻辑地址 转换成 物理地址?

    对每个进程而言,都有着自己的页表。页表的本质就是逻辑地址到物理地址的映射。

    分页存储中的逻辑地址的结构如下:

    这里写图片描述

    1)由于进程的逻辑页面大小与物理块(页帧)大小相同,故都为4K,因此需要12个位表示4K的大小(2^12=4K),即图中的【0-11】

    2)【12-31】表示的是页号。一共有20个位表示页号,也即:对于一个进程而言,一共可以有1M(2^20=1M)个页。

    3)每个进程的逻辑地址空间范围为0-2^32-1,因为:每个页大小为4K,一共有1M个页。故进程可用的逻辑空间为2^32B

    逻辑地址到物理地址的转换需要用到页表。具体细节是有一个“地址变换机构”,它有一个寄存器保存页表在内存的起始地址 以及 页表的长度。

    上面提到,一个进程最多可以有1M个页,故页表就有1M个页表项。假设每个页表项只有1B,那页表的大小也有1MB,所以:一般而言,页表也是很大的,不能全放在寄存器中,故页表也是存储在内存中的。(有些机器有“快表”,快表就是一个寄存器,它保存了页表中的部分表项);其次,也可以使用多级页表以解决单个页表太大的问题。

    那现在给定一个逻辑地址,怎么知道其物理地址呢?

    ①将【12-31】位的页号与 页表的长度比较。页号不能大于页表长度,否则越界。

    ②根据页号 找到 该页号所在的页表项,即该页号对应着哪个页表项。因为,页表项里面就存放着物理地址。

    那如何查找页表项呢?将页号乘以页表项的长度(每个页表项,其实就是一个逻辑的页 到 物理页 的映射信息),就知道了该逻辑页对应着哪个页表项(根据页号匹配页表项一般是由硬件完成的)

    然后,正如前面提到,页表也是保存在内存中的,故需要页表的内存始址(这是也为什么地址变换机构 保存 页表在内存的起始地址的原因),将页表始址 与 上面的乘积相加,就得到了该逻辑页对应的页表项的物理地址。读这个页表项的物理地址中的内容,就知道了该逻辑页对应的物理块地址(物理地址)。从而,就完成了逻辑地址到物理地址的转换。

    从上面可以看出,CPU每存取一个数据时,需要两次访问主存。一次是访问页表项的物理地址,得到了数据的物理块地址。第二次拿着物理块地址去取数据。

    在分页存储管理方式下:由于取一个数据,需要二次访存,CPU处理速度降低了一半,正由于这个原因:引入了“快表”(又称TLB(Translation Lookaside Buffer)),快表是个寄存器,用来保存那些当前访问过的页表项。从而,读页表项时,不需要再访存了,而是直接从寄存器中读取。

    虚拟存储器

    谈到虚拟存储器,总是说它从逻辑上扩充了内存的容量,why?

    内存是有限的,作业初始时保存在磁盘上的,如果要运行,必须得将相应的程序(数据)加载到内存中。那如果要运行的作业特别多,无法一下子装入内存,怎么办?

    一种方式是加内存条,这是从物理上扩充内存的容量。

    另一种方式是:先把作业的一部分程序(数据)装入内存,先让它运行着,运行过程中发现: 咦,我还需要其他的数据,而这些数据还未装入内存,因此就产生中断(缺页中断)再将数据加载到内存。

    采用这种方式,系统一次就可以将很多作业装入内存运行了。这时,从物理上看,内存还是原来的大小,但是它能运行的作业多了,因此说从逻辑上扩充了内存。

    将虚拟存储器这种思想与分页存储管理结合,一次只将作业的部分页面加载到内存中,形成了一个强大的内存分配与管理系统了。引入了虚拟存储器,同样需要有页表,记录逻辑地址到物理地址的映射,只不过此时的页表更复杂了,因为,有些页可能还在磁盘上。;还需要有缺页中断处理机构,因为毕竟只将一部分数据装入内存,会引起缺页中断嘛,就需要处理中断嘛;还需要地址变换机构,这里的地址变换机构功能更多,因为需要处理中断情况下的地址变换。

    转载自:https://www.cnblogs.com/hapjin/p/5689049.html

    展开全文
  • 连续内存分配与非连续内存分配

    千次阅读 2018-09-22 08:45:03
    连续内存分配 首次适配:空闲分区以地址递增的次序链接。分配内存时顺序查找,找到大小能满足要求的第一个空闲分区。 最优适配:空闲分区按容量递增形成分区链,找到第一个能满足要求的空闲分区。 最坏适配:空闲...

    连续内存分配

    1. 首次适配:空闲分区以地址递增的次序链接。分配内存时顺序查找,找到大小能满足要求的第一个空闲分区。
    2. 最优适配:空闲分区按容量递增形成分区链,找到第一个能满足要求的空闲分区。
    3. 最坏适配:空闲分区以容量递减的次序链接。找到第一个能满足要求的空闲分区,也就是挑选出最大的分区。

     

     

    非连续内存分配

    一、分页式存储管理介绍:

           用户程序逻辑地址空间->页(连续)

           物理内存空间->块(离散)

           页表(内存中):key=页号,value=块号(内存中的物理块)

           快表(高速缓冲区存储器中):key=页号,value=块号(内存中的物理块)

    1. 基本思想:用户程序的地址空间被划分成若干固定大小的区域,称为“页”,相应地,内存空间分成若干个物理块,页和块的大小相等。可将用户程序的任一页放在内存的任一块中,实现了离散分配。
    2. 分页概念:逻辑空间分页,物理空间分块,页与块同样大,页连续块离散,用页号查页表,由硬件做转换,页面和内存块大小一般选为2的若干次幂(便于管理)。页表作用:实现从页号到物理地址的映射
    3. 分页存储管理的地址机构:页号4位,每个作业最多2的4次方=16页,表示页号从0000~1111(24-1),页内位移量的位数表示页的大小,若页内位移量12位,则2的12次方=4k,页的大小为4k,页内地址从000000000000~111111111111
    4. 页表:分页系统中,允许将进程的每一页离散地存储在内存的任一物理块中,为了能在内存中找到每个页面对应的物理块,系统为每个进程建立一张页面映射表,简称页表。页表的作用是实现从页号到物理块号的地址映射。
    5. 具有快表的地址变换机构:分页系统中,CPU每次要存取一个数据,都要两次访问内存(访问页表、访问实际物理地址)。为提高地址变换速度,增设一个具有并行查询能力的特殊高速缓冲存储器,称为“联想存储器”或“快表”,存放当前访问的页表项,快表就是存放在高速缓冲存储器的部分页表。它起页表相同的作用。包含快表机制的内存管理中,当要访问内存数据的时候,首先将页号在快表中查询,如果查找到说明要访问的页表项在快表中,那么直接从快表中读取相应的物理块号;如果没有找到,那么访问内存中的页表,从页表中得到物理地址,同时将页表中的该映射表项添加到快表中(可能存在快表换出算法)。只需要一次内存访问
    6. 多级页表:在某些计算机中如果内存的逻辑地址很大,将会导致程序的页表项会很多,而页表在内存中是连续存放的,所以相应的就需要较大的连续内存空间。为了解决这个问题,可以采用两级页表或者多级页表的方法,其中外层页表一次性调入内存且连续存放,内层页表离散存放。相应的访问内存页表的时候需要一次地址变换,访问逻辑地址对应的物理地址的时候也需要一次地址变换,而且一共需要访问内存3次才可以读取一次数据。
    7. 如果存储器采用基本分页机制,那么操作系统会为每个进程或任务建立一个页表(这个页表可能是一级的也可能是多级的)。整个操作系统中有多个进程在运行,那么系统就会有多个页表。页表在内存中的存储位置由寄存器CR3给出。

     

    二、分段式存储管理

    1、基本思想:将用户程序地址空间分成若干个大小不等的段,每段可以定义一组相对完整的逻辑信息。存储分配时,以段为单位,段与段在内存中可以不相邻接,也实现了离散分配。

    2、分段存储方式的引入:方便编程、分段共享、分段保护、动态链接、动态增长

    3、分段地址结构:作业的地址空间被划分为若干个段,每个段定义了一组逻辑信息。例程序段、数据段等。每个段都从0开始编址,段长不一样,并采用一段连续的地址空间。段的长度由相应的逻辑信息组的长度决定,因而各段长度不等。整个作业的地址空间是二维的。一维是段号,一维是(段内地址/段表项的长度);由于分段管理中,每个段内部是连续内存分配,但是段和段之间是离散分配的,因此也存在一个逻辑地址到物理地址的映射关系,相应的就是段表机制。段表中的每一个表项记录了该段在内存中的起始地址和该段的长度。段表可以放在内存中也可以放在寄存器中。

    4、访问内存的次数:访问内存的时候根据段号和(段内地址/段表项的长度/段内位移量)计算当前访问段在段表中的位置,然后访问段表,得到该段的物理地址,根据该物理地址以及段内偏移量就可以得到需要访问的内存。由于也是两次内存访问,所以分段管理中同样引入了联想寄存器。

    5、如果存储器采用基本分段机制,那么操作系统会为每个进程或任务建立一个段表

    分段总结:作业空间是二维的,一维段号、一维段内地址/段内位移量(段长),段表由段号、段长、基址组成,用户通过段号到段表中获得段长与基址,从而定位到内存中的数据。内存空间中,每个段之间不连续,但是每个段自己内部是连续的

     

    三、分页和分段的主要区别(两者都不要求作业连续存放)

    1、段是信息的逻辑单位,分段的目的是为了更好地实现共享,根据用户的需要划分,因此段对用户是可见的;页是信息的物理单位,是为了管理主存的方便而划分的,分页是为了实现非连续分配,以便解决内存碎片问题,或者说分页是由于系统管理的需要,其对用户是透明的

    2、段的大小不固定,由它所完成的功能决定;页的大小固定(一般为4K),由系统决定,将逻辑地址划分为页号和页内地址是由机器硬件实现的。

    3、段向用户提供二维地址(段号+段内地址);页向用户提供的是一维地址(页号)

    4、段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享受到限制。

     

    四、段页式存储管理

    1、基本思想:

           分页系统能有效地提高内存的利用率,而分段系统能反映程序的逻辑结构,便于段的共享与保护,将分页与分段两种存储方式结合起来,就形成了段页式存储管理方式。

           在段页式存储管理系统中,作业的地址空间首先被分成若干个逻辑分段,每段都有自己的段号,然后再将每段分成若干个大小相等的页。对于主存空间也分成大小相等的页,主存的分配以页为单位。

           段页式系统中,作业的地址结构包含三部分的内容:段号、页号、页内位移量

           程序员按照分段系统的地址结构将地址分为段号与段内位移量,地址变换机构将段内位移量分解为页号和页内位移量。

           为实现段页式存储管理,系统应为每个进程设置一个段表,包括每段的段号,该段的页表始址和页表长度(本来是段内基址和段长),如下图。每个段有自己的页表,记录段中的每一页的页号和存放在主存中的物理块号。

           如果采用段页式结合的机制,那么一般一个进程或任务,操作系统会给其建立一个段表,而段表中的每个段又会对应一个页表,也就是说,段页式机制的每个进程有一个段表,有多个页表。

     

    2、地址变换的过程:

    (1)程序执行时,从PCB中取出段表始址和段表长度,装入段表寄存器。

    (2)由地址变换机构将逻辑地址自动分成段号、页号和页内地址。(涵盖了段表+页表)

    (3)将段号与段表长度进行比较,若段号大于或等于段表长度,则表示本次访问的地址已超越进程的地址空间,产生越界中断。

    (4)将段表始址与段号和段表项长度的乘积相加,便得到该段表项在段表中的位置。

    (5)取出段描述子得到该段的页表始址和页表长度。

    (6)将页号与页表长度进行比较,若页号大于或等于页表长度,则表示本次访问的地址已超越进程的地址空间,产生越界中断。

    (7)将页表始址与页号和页表项长度的乘积相加,便得到该页表项在页表中的位置。

    (8)取出页描述子得到该页的物理块号。

    (9)对该页的存取控制进行检查。

    (10)将物理块号送入物理地址寄存器中,再将有效地址寄存器中的页内地址直接送入物理地址寄存器的块内地址字段中,拼接得到实际的物理地址。

    3、访问内存的次数

           在段页式系统中,为了获得一条指令或数据,须三次访问内存。第一次访问是访问内

           存中的段表,从中取得页表始址;第二次访问是访问内存中的页表,从中取出该页所在的物理块号,并将该块号与页内地址一起形成指令或数据的物理地址;第三次访问才是真正从第二次访问所得的地址中,取出指令或数据。

     

    4、段页式总结:

           作业空间分段,再将每段分成大小相等的页,内存也分页

           作业空间是二维的,分段号、页号、页内偏移量

           程序员按照分段系统的地址结构将地址分为段号与段内位移量(段的长度),地址变换机构将段内位移量分解为页号和页内位移量

     

    五、在网上找到了一个比较形象的比喻,挺不错的,列出来如下:

           打个比方,比如说你去听课,带了一个纸质笔记本做笔记。笔记本有100张纸,课程有语文、数学、英语三门,对于这个笔记本的使用,为了便于以后复习方便,你可以有两种选择。

           第一种是,你从本子的第一张纸开始用,并且事先在本子上做划分:第2张到第30张纸记语文笔记,第31到60张纸记数学笔记,第61到100张纸记英语笔记,最后在第一张纸做个列表,记录着三门笔记各自的范围。这就是分段管理,第一张纸叫段表。

           第二种是,你从第二张纸开始做笔记,各种课的笔记是连在一起的:第2张纸是数学,第3张是语文,第4张英语……最后呢,你在第一张纸做了一个目录,记录着语文笔记在第3、7、14、15张纸……,数学笔记在第2、6、8、9、11……,英语笔记在第4、5、12……。这就是分页管理,第一张纸叫页表。你要复习哪一门课,就到页表里查寻相关的纸的编号,然后翻到那一页去复习

     

     

    来自码农翻身 关于非连续内存分配的理解介绍:

    虚拟内存:时间局部性原理+空间局部性原理

    分页:

    每页4KB,装载程序的时候按4KB装载

            物理内存分块/页框(Page Frame),虚拟地址空间分页

            操作系统维持一个页表,用来映射虚拟页面和物理页面

            如果访问到一个没有被映射到物理内存的页面,则产生缺页中断

                  地址分为两部分:页号+偏移量(分段机制),由CPU的MMU实现地址的转换

                  由页号p在内存中的页表找到对应的页框号/块f,再到程序的物理地址中找到f块,对应偏移量i就可以得到程序了

            为了防止每次都要多次访问内存,可以在MMU的缓存中存放快表

    分段+分页:段页式存储管理机制

           将程序分为代码段、数据段、堆栈段便于存储管理(比如修改代码段的时候可以立马抛出异常)

           地址分为段号+偏移量,通过段号找到段的基址,和偏移量相加得到一个线性地址,这个线性地址再通过分页系统进行转换,最后形成物理地址

     

    参考链接:

    http://link.zhihu.com/?target=http%3A//blog.csdn.net/wangrunmin/article/details/7967293

    展开全文
  • 图解Golang的内存分配

    万次阅读 2019-06-09 20:11:07
    一般程序的内存分配 在讲Golang的内存分配之前,让我们先来看看一般程序的内存分布情况: 以上是程序内存的逻辑分类情况。 我们再来看看一般程序的内存的真实(真实逻辑)图: Go的内存分配核心思想 Go是内置运行时的...
  • 文章目录一、连续内存分配1、内存碎片的问题(1)外部碎片(2)内部碎片2、连续内存分配算法(1)首次适配(2)最优适配(3)最差适配3、碎片整理方法4、连续内存分配的缺点二、非连续内存分配1、非连续分配的优点2...
  • 内存分配与回收策略

    万次阅读 2020-04-15 16:46:50
    内存分配与回收策略 对象的内存分配,就是在堆上分配(也可能经过 JIT 编译后被拆散为标量类型并间接在栈上分配),对象主要分配在新生代的 Eden 区上,少数情况下可能直接分配在老年代,分配规则不固定,取决于当前...
  • JVM进阶(三)——内存分配与回收策略

    万次阅读 多人点赞 2017-01-08 14:47:08
    JVM进阶(三)——内存分配与回收策略  各位小伙伴大家好,还记得上一博文《JVM进阶(二)——初识JAVA堆》我们讲了什么吗?虚拟机中的堆,他是整个内存模型中占用最大的一部分,而且不是连续的。当有需要分配内存的...
  • mapreduce内存分配

    千次阅读 2018-07-05 10:29:17
    稍微有点mapreduce使用经验的同学肯定对OOM不陌生,对的,我目前在mapReduce里面遇到的最多的报错也是内存分配出错,所以看到好多hadoop执行脚本里面有好多关于内存的参数,虽然是知道和内存分配有关系,但是我依然...
  • 前一段时间查看资料得知内存管理的算法大致有两种:内存分配算法、内存页面置换算法,对这些算法虽然可能不需要实现,但是需要了解其中的概念原理,以备不时之需。 内存分配的算法主要有5种: 第一种是固定内存分配...
  • C++内存分配方式

    千次阅读 2020-08-27 21:49:06
    内存分配运算内置于处理器的指令集中,效率高,分配的内存容量有限。 堆,就是那些由malloc等分配的内存块,用free来释放内存。 自由存储区,那些由new分配的内存块,由应用程序去控制,一般一个new就要对应一个...
  • C#堆内存分配和栈内存分配

    千次阅读 2015-08-24 14:46:55
    C#堆内存分配和栈内存分配  五大内存分区  在C#中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。 栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的...
  • 在涉及到内存分配时,我们一般都要考虑到两种内存分配方式,一种是动态内存分配,另一种是静态内存分配,我们该怎么理解这两者的区别呢? 在我看来,静态内存分配和动态内存分配比较典型的例子就是数组和链表,数组...
  • 结构体内存分配

    千次阅读 2018-11-07 14:41:39
    ,经过学习,特把有关结构体内存分配的相关知识记录下来。 内存对齐:要了解结构体的内存分配,首先需要了解内存对齐的概念。之所以会有内存对齐的概念,是因为,在操作系统中,数据的存放时按照字节存放的,而...
  • #include&lt;stdio.h&...二、动态内存分配函数  malloc :从堆上分配内存  realloc : 在之前分配的内存块的基础上,将内存重新分配为更大或者更小的部分  calloc: 从堆上分配内存并清零  fre...
  • CMA内存分配

    千次阅读 2018-04-04 11:01:28
    在以前的驱动中,一般给LCD/GPU/camera预留部分内存,满足这些模块内存分配的同时,也能够提供物理地址连续内存. 但是,如果GPU/camera/LCD没有使用时,这部分内存就白白浪费了,所以就有了CMA机制.   CMA给驱动...
  • Java内存区域划分和内存分配策略

    千次阅读 多人点赞 2020-05-15 12:32:46
    Java内存区域划分和内存分配策略 如果不知道,类的静态变量存储在那? 方法的局部变量存储在那? 赶快收藏 Java内存区域主要可以分为共享内存,堆、方法区和线程私有内存,虚拟机栈、本地方法栈和程序计数器。如下...
  • slab内存分配器是linux内核中比较经典的内存分配器,目前已经被slub内存分配器取代了。不过为了致敬经典,更是为理解slub分配器做铺垫,我还是会从slab分配器讲起。 为什么需要slab内存分配器? 这个问题其实...
  • C内存分配方式与C++内存分配方式

    千次阅读 2016-03-23 17:22:12
    C内存分配方式: 栈区(stack)—由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆区(heap)—一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。...
  • ESXi 内存分配原理

    千次阅读 2019-11-05 10:28:02
    运行在ESXi主机上的虚拟机分配内存之和可以超过物理机的实际内存大小,这个技术叫做超额分配(overcommitment),即使单个虚拟机的内存分配值都可以超分。但是超分的结果就是可能会引起内存资源竞争,从而有可能影响...
  • C语言内存分配

    万次阅读 2017-08-17 12:17:19
    Objective-C从名字来看就可以知道是一门超C语言,所以了解C语言的内存模型对于理解Objective-C的内存管理有很大的帮助。C语言内存模型图如下:从图中可以看出...栈内存分配运算内置于处理器的指令集,效率很高,但是
  • JVM系列之内存分配与回收策略

    万次阅读 2020-06-05 19:58:46
    前言 经过前面几篇文章,我们已经对JVM虚拟机有了个初步认识,...内存分配,通俗讲就是对象在堆上分配:对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数对象也可能直接.
  • c语言动态内存分配

    千次阅读 2019-01-03 23:00:21
    动态内存分配是许多高级程序设计技巧的关键。 所有程序都必须留下足够的内存来存储程序使用的数据。这些内存中,有些是自动分配的(栈),有些是在编译时就确定好的(静态区),c语言还可以实现动态的/运行时的内存...
  • JVM内存分配策略

    千次阅读 2019-01-23 18:00:34
    JVM内存分配策略 java的内存自动管理可以总结为自动分配和自动回收内存。 对象内存的分配主要是在堆上分配内存,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程有限分配在TLAB上。少数...
  • Redis 的内存分配

    千次阅读 2018-09-10 22:55:23
    今天这篇文章主要分享一下Redis的内存分配和简单的查看Redis内存使用情况。 内存分配 Redis进程的内存消耗主要包括:自身内存 + 对象内存 + 缓冲内存 + 内存碎片。 1 自身内存 Redis自身内存消耗非常少,...
  • WebLogic内存分配

    千次阅读 2018-06-28 10:55:15
    通过修改commom/bin/commEnv.cmd文件来增加内存分配。 修改的部分如下::beaif "%PRODUCTION_MODE%" == "true" goto bea_prod_modeset JAVA_VM=-jrockitset MEM_ARGS=-...
  • 内存分配算法

    千次阅读 2019-04-25 00:15:02
    内存分配算法: (1)首次适应算法(First Fit):从空闲分区表的第一个表目起查找该表,把最先能够满足要求的空闲区分配给作业,这种方法的目的在于减少查找时间。为适应这种算法,空闲分区表(空闲区链)中的空闲...
  • golang 内存分配

    千次阅读 2018-03-22 11:53:59
     从上述结构图来看,内存分配器还是有一点小复杂的,但根据具体的逻辑层次可以拆分成三大模块--cache,central,heap,然后一个一个的模块分析下去,逻辑就显得特别清晰明了了。位于结构图最下边的Cache就是cache...
  • c 程序内存分配管理

    千次阅读 2018-09-23 15:44:33
    给变量分配内存空间可分为静态内存分配和动态内存分配。 静态内存分配属于编译时给变量分配的空间,动态分配属于在程序运行时给变量分配的空间 静态分配属于栈分配,动态分配属于堆分配 运行效率上,静态内存比...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 155,872
精华内容 62,348
关键字:

内存分配