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

    千次阅读 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 书籍。

    展开全文
  • 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

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


    在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后奇偶内核才能检测到可用内存和寄存器.

    1 前景回顾


    1.1 Linux内存管理的层次结构


    Linux把物理内存划分为三个层次来管理

    层次描述
    存储节点(Node)CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点
    管理区(Zone)每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存
    页面(Page)内存被细分为多个页面帧, 页面是最基本的页面分配的单位 |


    为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点

    • 首先, 内存被划分为结点. 每个节点关联到系统中的一个处理器, 内核中表示为pg_data_t的实例. 系统中每个节点被链接到一个以NULL结尾的pgdat_list链表中<而其中的每个节点利用pg_data_tnode_next字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构.

    • 接着各个节点又被划分为内存管理区域, 一个管理区域通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射.

    • 最后页帧(page frame)代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号.

    1.2 今日内容(启动过程中的内存初始化)


    在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉.

    因此我们可以把linux内核的内存管理分三个阶段。

    阶段起点终点描述
    第一阶段系统启动bootmem或者memblock初始化完成此阶段只能使用memblock_reserve函数分配内存, 早期内核中使用init_bootmem_done = 1标识此阶段结束
    第二阶段bootmem或者memblock初始化完buddy完成前引导内存分配器bootmem或者memblock接受内存的管理工作, 早期内核中使用mem_init_done = 1标记此阶段的结束
    第三阶段buddy初始化完成系统停止运行可以用cache和buddy分配内存

    1.3 start_kernel系统启动阶段的内存初始化过程


    首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在init/main.c?v=4.7, line 479

    其代码很复杂, 我们只截取出其中与内存管理初始化相关的部分, 如下所示

    asmlinkage __visible void __init start_kernel(void)
    {
    
        setup_arch(&command_line);
        mm_init_cpumask(&init_mm);
    
        setup_per_cpu_areas();
    
    
        build_all_zonelists(NULL, NULL);
        page_alloc_init();
    
    
        /*
         * These use large bootmem allocations and must precede
         * mem_init();
         * kmem_cache_init();
         */
        mm_init();
    
        kmem_cache_init_late();
    
        kmemleak_init();
        setup_per_cpu_pageset();
    
        rest_init();
    }
    函数功能
    setup_arch是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器
    mm_init_cpumask初始化CPU屏蔽字
    setup_per_cpu_areas函数(查看定义)给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间.
    在SMP系统中, setup_per_cpu_areas初始化源代码中(使用per_cpu宏)定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本.
    此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本
    在非SMP系统中这是一个空操作
    build_all_zonelists建立并初始化结点和内存域的数据结构
    mm_init建立了内核的内存分配器,
    其中通过mem_init停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)
    然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器
    kmem_cache_init_late在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器slab, slob, slub都会定义此函数
    kmemleak_initKmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 /sys/kernel/debug/kmemleak中, Kmemcheck能够帮助定位大多数内存错误的上下文
    setup_per_cpu_pageset初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配
    由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中.

    1.4 setup_arch函数初始化内存流程


    前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作.

    void __init setup_arch(char **cmdline_p)
    {
        /*  初始化memblock  */
        arm64_memblock_init( );
    
        /*  分页机制初始化  */
        paging_init();
    
        bootmem_init();
    }
    流程描述
    arm64_memblock_init初始化memblock内存分配器
    paging_init初始化分页机制
    bootmem_init初始化内存管理

    该函数主要执行了如下操作

    1. 使用arm64_memblock_init来完成memblock机制的初始化工作, 至此memblock分配器接受系统中系统中内存的分配工作

    2. 调用paging_init来完成系统分页机制的初始化工作, 建立页表, 从而内核可以完成虚拟内存的映射和转换工作

    3. 最后调用bootmem_init来完成实现buddy内存管理所需要的工作

    1.5 (第一阶段)启动过程中的内存分配器


    在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉.

    这个阶段的内存分配其实很简单, 因此我们往往称之为内存分配器(而不是内存管理器), 早期的内核中内存分配器使用的bootmem引导分配器, 它基于一个内存位图bitmap, 使用最优适配算法来查找内存, 但是这个分配器有很大的缺陷, 最严重的就是内存碎片的问题, 因此在后来的内核中将其舍弃《而使用了新的memblock机制. memblock机制的初始化在arm64上是通过arm64_memblock_init函数来实现的

    ```cpp
    start_kernel()
        |---->page_address_init()
        |     考虑支持高端内存
        |     业务:初始化page_address_pool链表;
        |          将page_address_maps数组元素按索引降序插入
        |          page_address_pool链表; 
        |          初始化page_address_htable数组.
        | 
        |---->setup_arch(&command_line);
        |     初始化特定体系结构的内容
              |
              |---->arm64_memblock_init( );
              |     初始化引导阶段的内存分配器memblock
              |
              |---->paging_init();
              |     分页机制初始化
              |
              |---->bootmem_init();   [当前位置]
              |     始化内存数据结构包括内存节点, 内存域和页帧page
                    |
                    |---->arm64_numa_init();
                    |     支持numa架构
                    |
                    |---->zone_sizes_init(min, max);
                        来初始化节点和管理区的一些数据项
                        |
                        |---->free_area_init_node
                        |   初始化内存节点
                        |
                            |---->free_area_init_core
                                |   初始化zone
                                |
                                |---->memmap_init
                                |   初始化page页面
                    |
                    |---->memblock_dump_all();
                    |   初始化完成, 显示memblock的保留的所有内存信息
                   |
        |---->build_all_zonelist()
        |     为系统中的zone建立后备zone的列表.
        |     所有zone的后备列表都在
        |     pglist_data->node_zonelists[0]中;
        |
        |     期间也对per-CPU变量boot_pageset做了初始化. 
        |

    1.6 今日内容(第二阶段(一)–初始化内存管理数据结构)


    我们之前讲了在memblock完成之后, 内存初始化开始进入第二阶段, 第二阶段是一个漫长的过程, 它执行了一系列复杂的操作, 从体系结构相关信息的初始化慢慢向上层展开, 其主要执行了如下操作

    特定于体系结构的设置

    在完成了基础的内存结点和内存域的初始化工作以后, 我们必须克服一些硬件的特殊设置

    • 在初始化内存的结点和内存区域之前, 内核先通过pagging_init初始化了内核的分页机制, 这样我们的虚拟运行空间就初步建立, 并可以完成物理地址到虚拟地址空间的映射工作.

    在arm64架构下, 内核在start_kernel()->setup_arch()中通过arm64_memblock_init( )完成了memblock的初始化之后, 接着通过setup_arch()->paging_init()开始初始化分页机制

    paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响

    • 在分页机制完成后, 内核通过setup_arch()->bootmem_init开始进行内存基本数据结构(内存结点pg_data_t, 内存域zone和页帧)的初始化工作, 就是在这个函数中, 内核开始从体系结构相关的部分逐渐展开到体系结构无关的部分, 在zone_sizes_init->free_area_init_node中开始, 内核开始进行内存基本数据结构的初始化, 也不再依赖于特定体系结构无关的层次
    bootmem_init()
    始化内存数据结构包括内存节点, 内存域和页帧page
    |
    |---->arm64_numa_init();
    |     支持numa架构
    |
    |---->zone_sizes_init(min, max);
        来初始化节点和管理区的一些数据项
        |
        |---->free_area_init_node
        |   初始化内存节点
        |
            |---->free_area_init_core
                |   初始化zone
                |
                |---->memmap_init
                |   初始化page页面
    |
    |---->memblock_dump_all();
    |   初始化完成, 显示memblock的保留的所有内存信息

    建立内存管理的数据结构

    对相关数据结构的初始化是从全局启动函数start_kernel中开始的, 该函数在加载内核并激活各个子系统之后执行. 由于内存管理是内核一个非常重要的部分, 因此在特定体系结构的设置步骤中检测并确定系统中内存的分配情况后, 会立即执行内存管理的初始化.

    移交早期的分配器到内存管理器

    最后我们的内存管理器已经初始化并设置完成, 可以投入运行了, 因此内核将内存管理的工作从早期的内存分配器(bootmem或者memblock)移交到我们的buddy伙伴系统.

    2 初始化前的准备工作


    2.1 回到setup_arch函数(当前已经完成的工作)


    现在我们回到start_kernel()->setup_arch()函数

    void __init setup_arch(char **cmdline_p)
    {
        /*  初始化memblock  */
        arm64_memblock_init( );
    
        /*  分页机制初始化  */
        paging_init();
    
        bootmem_init();
    }

    到目前位置我们已经完成了如下工作

    • memblock已经通过arm64_memblock_init完成了初始化, 至此系统中的内存可以通过memblock分配了

    • paging_init完成了分页机制的初始化, 至此内核已经布局了一套完整的虚拟内存空间

    至此我们所有的内存都可以通过memblock机制来分配和释放, 尽管它实现的笨拙而简易, 但是已经足够我们初始化阶段使用了, 反正内核页不可能指着它过一辈子, 而我们也通过pagging_init创建了页表, 为内核提供了一套可供内核和进程运行的虚拟运行空间, 我们可以安全的进行内存的分配了

    因此该是时候初始化我们强大的buddy系统了.

    内核接着setup_arch()->bootmem_init()函数开始执行

    体系结构相关的代码需要在启动期间建立如下信息

    • 系统中各个内存域的页帧边界,保存在max_zone_pfn数组

    早期的内核还需记录各结点页帧的分配情况,保存在全局变量early_node_map中

    zone_sizes_init函数

    内核提供了一个通用的框架, 用于将上述信息转换为伙伴系统预期的节点和内存域数据结构, 但是在此之前各个体系结构必须自行建立相关结构.

    2.2 bootmem_init函数初始化内存结点和管理域


    arm64架构下, 在setup_arch中通过paging_init函数初始化内核分页机制之后, 内核通过bootmem_init()开始完成内存结点和内存区域的初始化工作, 该函数定义在arch/arm64/mm/init.c, line 306

    void __init bootmem_init(void)
    {
        unsigned long min, max;
    
        min = PFN_UP(memblock_start_of_DRAM());
        max = PFN_DOWN(memblock_end_of_DRAM());
    
        early_memtest(min << PAGE_SHIFT, max << PAGE_SHIFT);
    
        max_pfn = max_low_pfn = max;
    
        arm64_numa_init();
        /*
         * Sparsemem tries to allocate bootmem in memory_present(), so must be
         * done after the fixed reservations.
         */
        arm64_memory_present();
    
        sparse_init();
        zone_sizes_init(min, max);
    
        high_memory = __va((max << PAGE_SHIFT) - 1) + 1;
        memblock_dump_all();
    }

    2.3 zone_sizes_init函数


    在初始化内存结点和内存域之前, 内核首先通过setup_arch()–>bootmem_init()–>zone_sizes_init()来初始化节点和管理区的一些数据项, 其中关键的是初始化了系统中各个内存域的页帧边界,保存在max_zone_pfn数组.

    zone_sizes_init函数定义在arch/arm64/mm/init.c?v=4.7, line 92, 由于arm64支持NUMA和UMA两种存储器架构, 因此该函数依照NUMA和UMA, 有两种不同的实现.

    #ifdef CONFIG_NUMA
    
    static void __init zone_sizes_init(unsigned long min, unsigned long max)
    {
        unsigned long max_zone_pfns[MAX_NR_ZONES]  = {0};
    
        if (IS_ENABLED(CONFIG_ZONE_DMA))
            max_zone_pfns[ZONE_DMA] = PFN_DOWN(max_zone_dma_phys());
        max_zone_pfns[ZONE_NORMAL] = max;
    
        free_area_init_nodes(max_zone_pfns);
    }
    
    #else
    
    static void __init zone_sizes_init(unsigned long min, unsigned long max)
    {
        struct memblock_region *reg;
        unsigned long zone_size[MAX_NR_ZONES], zhole_size[MAX_NR_ZONES];
        unsigned long max_dma = min;
    
        memset(zone_size, 0, sizeof(zone_size));
    
        /* 4GB maximum for 32-bit only capable devices */
    #ifdef CONFIG_ZONE_DMA
        max_dma = PFN_DOWN(arm64_dma_phys_limit);
        zone_size[ZONE_DMA] = max_dma - min;
    #endif
        zone_size[ZONE_NORMAL] = max - max_dma;
    
        memcpy(zhole_size, zone_size, sizeof(zhole_size));
    
        for_each_memblock(memory, reg) {
            unsigned long start = memblock_region_memory_base_pfn(reg);
            unsigned long end = memblock_region_memory_end_pfn(reg);
    
            if (start >= max)
                continue;
    
    #ifdef CONFIG_ZONE_DMA
            if (start < max_dma) {
                unsigned long dma_end = min(end, max_dma);
                zhole_size[ZONE_DMA] -= dma_end - start;
            }
    #endif
            if (end > max_dma) {
                unsigned long normal_end = min(end, max);
                unsigned long normal_start = max(start, max_dma);
                zhole_size[ZONE_NORMAL] -= normal_end - normal_start;
            }
        }
    
        free_area_init_node(0, zone_size, min, zhole_size);
    }
    
    #endif /* CONFIG_NUMA */

    在获取了三个管理区的页面数后, NUMA架构下通过free_area_init_nodes()来完成后续工作, 其中核心函数为free_area_init_node(),用来针对特定的节点进行初始化, 由于UMA架构下只有一个内存结点, 因此直接通过free_area_init_node来完成内存结点的初始化

    截至到目前为止, 体系结构相关的部分已经结束了, 各个体系结构已经自行建立了自己所需的一些底层数据结构, 这些结构建立好以后, 内核将繁重的内存数据结构创建和初始化的工作交给free_area_init_node(s)函数来完成,

    3 free_area_init_nodes初始化NUMA管理数据结构


    注意

    此部分内容参照

    Linux内存管理伙伴算法

    linux 内存管理 - paging_init 函数

    free_area_init_nodes初始化了NUMA系统中所有结点的pg_data_t和zone、page的数据, 并打印了管理区信息, 该函数定义在mm/page_alloc.c?v=4.7, line 6460

    3.1 代码注释


    //  初始化各个节点的所有pg_data_t和zone、page的数据
    void __init free_area_init_nodes(unsigned long *max_zone_pfn)
    {
        unsigned long start_pfn, end_pfn;
        int i, nid;
    
        /* Record where the zone boundaries are
         * 全局数组arch_zone_lowest_possible_pfn
         * 用来存储各个内存域可使用的最低内存页帧编号   */
        memset(arch_zone_lowest_possible_pfn, 0,
                    sizeof(arch_zone_lowest_possible_pfn));
    
        /* 全局数组arch_zone_highest_possible_pfn
         * 用来存储各个内存域可使用的最高内存页帧编号   */
        memset(arch_zone_highest_possible_pfn, 0,
                    sizeof(arch_zone_highest_possible_pfn));
    
        /* 辅助函数find_min_pfn_with_active_regions
         * 用于找到注册的最低内存域中可用的编号最小的页帧 */
        arch_zone_lowest_possible_pfn[0] = find_min_pfn_with_active_regions();
    
        /*  max_zone_pfn记录了各个内存域包含的最大页帧号  */
        arch_zone_highest_possible_pfn[0] = max_zone_pfn[0];
    
        /*  依次遍历,确定各个内存域的边界    */
        for (i = 1; i < MAX_NR_ZONES; i++) {
            /*  由于ZONE_MOVABLE是一个虚拟内存域
             *  不与真正的硬件内存域关联
             *  该内存域的边界总是设置为0 */
            if (i == ZONE_MOVABLE)
                continue;
            /*  第n个内存域的最小页帧
             *  即前一个(第n-1个)内存域的最大页帧  */
            arch_zone_lowest_possible_pfn[i] =
                arch_zone_highest_possible_pfn[i-1];
            /*  不出意外,当前内存域的最大页帧
             *  由max_zone_pfn给出  */
            arch_zone_highest_possible_pfn[i] =
                max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]);
        }
        arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0;
        arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0;
    
        /* Find the PFNs that ZONE_MOVABLE begins at in each node */
        memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn));
        /*  用于计算进入ZONE_MOVABLE的内存数量  */
        find_zone_movable_pfns_for_nodes();
    
        /* Print out the zone ranges
         * 将各个内存域的最大、最小页帧号显示出来  */
        pr_info("Zone ranges:\n");
        for (i = 0; i < MAX_NR_ZONES; i++) {
            if (i == ZONE_MOVABLE)
                continue;
            pr_info("  %-8s ", zone_names[i]);
            if (arch_zone_lowest_possible_pfn[i] ==
                    arch_zone_highest_possible_pfn[i])
                pr_cont("empty\n");
            else
                pr_cont("[mem %#018Lx-%#018Lx]\n",
                    (u64)arch_zone_lowest_possible_pfn[i]
                        << PAGE_SHIFT,
                    ((u64)arch_zone_highest_possible_pfn[i]
                        << PAGE_SHIFT) - 1);
        }
    
        /* Print out the PFNs ZONE_MOVABLE begins at in each node */
        pr_info("Movable zone start for each node\n");
        for (i = 0; i < MAX_NUMNODES; i++) {
            /*  对每个结点来说,zone_movable_pfn[node_id]
             *  表示ZONE_MOVABLE在movable_zone内存域中所取得内存的起始地址
             *  内核确保这些页将用于满足符合ZONE_MOVABLE职责的内存分配 */
            if (zone_movable_pfn[i])
            {
                /*  显示各个内存域的分配情况  */
                pr_info("  Node %d: %#018Lx\n", i,
                       (u64)zone_movable_pfn[i] << PAGE_SHIFT);
            }
        }
    
        /* Print out the early node map */
        pr_info("Early memory node ranges\n");
        for_each_mem_pfn_range(i, MAX_NUMNODES, &start_pfn, &end_pfn, &nid)
            pr_info("  node %3d: [mem %#018Lx-%#018Lx]\n", nid,
                (u64)start_pfn << PAGE_SHIFT,
                ((u64)end_pfn << PAGE_SHIFT) - 1);
    
        /* Initialise every node */
        mminit_verify_pageflags_layout();
        setup_nr_node_ids();
    
        /*  代码遍历所有的活动结点,
         *  并分别对各个结点调用free_area_init_node建立数据结构,
         *  该函数需要结点第一个可用的页帧作为一个参数,
         *  而find_min_pfn_for_node则从early_node_map数组提取该信息   */
        for_each_online_node(nid) {
            pg_data_t *pgdat = NODE_DATA(nid);
            free_area_init_node(nid, NULL,
                    find_min_pfn_for_node(nid), NULL);
    
            /* Any memory on that node
             * 根据node_present_pages字段判断结点具有内存
             * 则在结点位图中设置N_HIGH_MEMORY标志
             * 该标志只表示结点上存在普通或高端内存
             * 因此check_for_regular_memory
             * 进一步检查低于ZONE_HIGHMEM的内存域中是否有内存
             * 并据此在结点位图中相应地设置N_NORMAL_MEMORY   */
            if (pgdat->node_present_pages)
                node_set_state(nid, N_MEMORY);
            check_for_memory(pgdat, nid);
        }
    }

    free_area_init_nodes函数中通过循环遍历各个节点,循环中调用了free_area_init_node函数初始化该节点对应的pg_data_t和zone、page的数据.

    3.2 设置可使用的页帧编号


    free_area_init_nodes首先必须分析并改写特定于体系结构的代码提供的信息。其中,需要对照在zone_max_pfn和zone_min_pfn中指定的内存域的边界,计算各个内存域可使用的最低和最高的页帧编号。使用了两个全局数组来存储这些信息:

    参见mm/page_alloc.c?v=4.7, line 259)

    static unsigned long __meminitdata arch_zone_lowest_possible_pfn[MAX_NR_ZONES];
    
    static unsigned long __meminitdata arch_zone_highest_possible_pfn[MAX_NR_ZONES];

    通过max_zone_pfn传递给free_area_init_nodes的信息记录了各个内存域包含的最大页帧号。
    free_area_init_nodes将该信息转换为一种更方便的表示形式,即以[low, high]形式描述各个内
    存域的页帧区间,存储在前述的全局变量中(我省去了对这些变量填充字节0的初始化过程):

    void __init free_area_init_nodes(unsigned long *max_zone_pfn)
    {
        /*  ......  */
        arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0;
        arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0;
    
        /* Find the PFNs that ZONE_MOVABLE begins at in each node */
        memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn));
        /*  用于计算进入ZONE_MOVABLE的内存数量  */
        find_zone_movable_pfns_for_nodes();
        /*  依次遍历,确定各个内存域的边界    */
        for (i = 1; i < MAX_NR_ZONES; i++) {
            /*  由于ZONE_MOVABLE是一个虚拟内存域
             *  不与真正的硬件内存域关联
             *  该内存域的边界总是设置为0 */
            if (i == ZONE_MOVABLE)
                continue;
            /*  第n个内存域的最小页帧
             *  即前一个(第n-1个)内存域的最大页帧  */
            arch_zone_lowest_possible_pfn[i] =
                arch_zone_highest_possible_pfn[i-1];
            /*  不出意外,当前内存域的最大页帧
             *  由max_zone_pfn给出  */
            arch_zone_highest_possible_pfn[i] =
                max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]);
        }
    
        /*  ......  */
    }

    辅助函数find_min_pfn_with_active_regions用于找到注册的最低内存域中可用的编号最小的页帧。该内存域不必一定是ZONE_DMA,例如,在计算机不需要DMA内存的情况下也可以是ZONE_NORMAL。最低内存域的最大页帧号可以从max_zone_pfn提供的信息直接获得。

    3.3 构建其他内存域的页帧区间


    接下来构建其他内存域的页帧区间,方法很直接:第n个内存域的最小页帧,即前一个(第n-1个)内存域的最大页帧。当前内存域的最大页帧由max_zone_pfn给出

    void __init free_area_init_nodes(unsigned long *max_zone_pfn)
    {
        /*  ......  */
    
        arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0;
        arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0;
    
        /* Find the PFNs that ZONE_MOVABLE begins at in each node */
        memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn));
        /*  用于计算进入ZONE_MOVABLE的内存数量  */
        find_zone_movable_pfns_for_nodes();
    
        /*  ......  */
    }

    由于ZONE_MOVABLE是一个虚拟内存域,不与真正的硬件内存域关联,该内存域的边界总是设置为0。回忆前文,可知只有在指定了内核命令行参数kernelcore或movablecore之一时,该内存域才会存在.
    该内存域一般开始于各个结点的某个特定内存域的某一页帧号。相应的编号在find_zone_movable_pfns_for_nodes里计算。

    现在可以向用户提供一些有关已确定的页帧区间的信息。举例来说,其中可能包括下列内容(输出取自AMD64系统,有4 GiB物理内存):

    > dmesg
    
    Zone PFN ranges:
    DMA 0 0 -> 4096
    DMA32 4096 -> 1048576
    Normal 1048576 -> 1245184

    3.4 建立结点数据结构


    free_area_init_nodes剩余的部分遍历所有结点,分别建立其数据结构

    void __init free_area_init_nodes(unsigned long *max_zone_pfn)
    {
        /*  输出有关内存域的信息  */
        /*  ......  */
    
        /*  代码遍历所有的活动结点,
         *  并分别对各个结点调用free_area_init_node建立数据结构,
         *  该函数需要结点第一个可用的页帧作为一个参数,
         *  而find_min_pfn_for_node则从early_node_map数组提取该信息   */
        for_each_online_node(nid) {
            pg_data_t *pgdat = NODE_DATA(nid);
            free_area_init_node(nid, NULL,
                    find_min_pfn_for_node(nid), NULL);
    
            /* Any memory on that node
             * 根据node_present_pages字段判断结点具有内存
             * 则在结点位图中设置N_HIGH_MEMORY标志
             * 该标志只表示结点上存在普通或高端内存
             * 因此check_for_regular_memory
             * 进一步检查低于ZONE_HIGHMEM的内存域中是否有内存
             * 并据此在结点位图中相应地设置N_NORMAL_MEMORY   */
            if (pgdat->node_present_pages)
                node_set_state(nid, N_MEMORY);
            check_for_memory(pgdat, nid);
        }
    
        /*  ......  */
    }

    代码遍历所有活动结点,并分别对各个结点调用free_area_init_node建立数据结构。该函数需要结点第一个可用的页帧作为一个参数,而find_min_pfn_for_node则从early_node_map数组提取该信息。

    如果根据node_present_pages字段判断结点具有内存,则在结点位图中设置N_HIGH_MEMORY标志。我们知道该标志只表示结点上存在普通或高端内存,因此check_for_regular_memory进一步检查低于ZONE_HIGHMEM的内存域中是否有内存,并据此在结点位图中相应地设置N_NORMAL_MEMORY标志

    4 free_area_init_node初始化UMA内存结点


    free_area_init_nodes函数初始化所有结点的pg_data_t和zone、page的数据,并打印了管理区信息.

    4.1 free_area_init_node函数注释


    该函数定义在mm/page_alloc.c?v=4.7, line 6076

    void __paginginit free_area_init_node(int nid, unsigned long *zones_size,
            unsigned long node_start_pfn, unsigned long *zholes_size)
    {
        pg_data_t *pgdat = NODE_DATA(nid);
        unsigned long start_pfn = 0;
        unsigned long end_pfn = 0;
    
        /* pg_data_t should be reset to zero when it's allocated */
        WARN_ON(pgdat->nr_zones || pgdat->classzone_idx);
    
        reset_deferred_meminit(pgdat);
        pgdat->node_id = nid;
        pgdat->node_start_pfn = node_start_pfn;
    #ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
        get_pfn_range_for_nid(nid, &start_pfn, &end_pfn);
        pr_info("Initmem setup node %d [mem %#018Lx-%#018Lx]\n", nid,
            (u64)start_pfn << PAGE_SHIFT,
            end_pfn ? ((u64)end_pfn << PAGE_SHIFT) - 1 : 0);
    #else
        start_pfn = node_start_pfn;
    #endif
        /*  首先累计各个内存域的页数
         *  计算结点中页的总数
         *  对连续内存模型而言
         *  这可以通过zone_sizes_init完成
         *  但calculate_node_totalpages还考虑了内存空洞 */
        calculate_node_totalpages(pgdat, start_pfn, end_pfn,
                      zones_size, zholes_size);
        /*  分配了该节点的页面描述符数组
         *  [pgdat->node_mem_map数组的内存分配  */
        alloc_node_mem_map(pgdat);
    #ifdef CONFIG_FLAT_NODE_MEM_MAP
        printk(KERN_DEBUG "free_area_init_node: node %d, pgdat %08lx, node_mem_map %08lx\n",
            nid, (unsigned long)pgdat,
            (unsigned long)pgdat->node_mem_map);
    #endif
    
        /*  对该节点的每个区[DMA,NORMAL,HIGH]的的结构进行初始化  */
        free_area_init_core(pgdat);
    }

    4.2 流程分析


    • calculate_node_totalpages函数累计各个内存域的页数,计算结点中页的总数。对连续内存模型而言,这可以通过zone_sizes_init完成,但calculate_node_totalpages还考虑了内存空洞,该函数定义在mm/page_alloc.c, line 5789

      以下例子取自一个UMA系统, 具有512 MiB物理内存。

    > dmesg
    ...
    On node 0 totalpages: 131056
    • alloc_node_mem_map(pgdat)函数分配了该节点的页面描述符数组[pgdat->node_mem_map数组的内存分配.

    • 继续调用free_area_init_core函数,继续初始化该节点的pg_data_t结构,初始化zone以及page结构 ,##2.6 free_area_init_core函数是初始化zone的核心

    4.3 alloc_node_mem_map函数


    alloc_node_mem_map负责初始化一个简单但非常重要的数据结构。如上所述,系统中的各个物理内存页,都对应着一个struct page实例。该结构的初始化由alloc_node_mem_map执行

    static void __init_refok alloc_node_mem_map(struct pglist_data *pgdat)
    {
        unsigned long __maybe_unused start = 0;
        unsigned long __maybe_unused offset = 0;
    
        /* Skip empty nodes */
        if (!pgdat->node_spanned_pages)
            return;
    
    #ifdef CONFIG_FLAT_NODE_MEM_MAP
        start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES - 1);
        offset = pgdat->node_start_pfn - start;
        /* ia64 gets its own node_mem_map, before this, without bootmem */
        if (!pgdat->node_mem_map) {
            unsigned long size, end;
            struct page *map;
    
            /*
             * The zone's endpoints aren't required to be MAX_ORDER
             * aligned but the node_mem_map endpoints must be in order
             * for the buddy allocator to function correctly.
             */
            end = pgdat_end_pfn(pgdat);
            end = ALIGN(end, MAX_ORDER_NR_PAGES);
            size =  (end - start) * sizeof(struct page);
            map = alloc_remap(pgdat->node_id, size);
            if (!map)
                map = memblock_virt_alloc_node_nopanic(size,
                                       pgdat->node_id);
            pgdat->node_mem_map = map + offset;
        }
    #ifndef CONFIG_NEED_MULTIPLE_NODES
        /*
         * With no DISCONTIG, the global mem_map is just set as node 0's
         */
        if (pgdat == NODE_DATA(0)) {
            mem_map = NODE_DATA(0)->node_mem_map;
    #if defined(CONFIG_HAVE_MEMBLOCK_NODE_MAP) || defined(CONFIG_FLATMEM)
            if (page_to_pfn(mem_map) != pgdat->node_start_pfn)
                mem_map -= offset;
    #endif /* CONFIG_HAVE_MEMBLOCK_NODE_MAP */
        }
    #endif
    #endif /* CONFIG_FLAT_NODE_MEM_MAP */
    }

    没有页的空结点显然可以跳过。如果特定于体系结构的代码尚未建立内存映射(这是可能的,例如,在IA-64系统上),则必须分配与该结点关联的所有struct page实例所需的内存。各个体系结构可以为此提供一个特定的函数。但目前只有在IA-32系统上使用不连续内存配置时是这样。在所有其他的配置上,则使用普通的自举内存分配器进行分配。请注意,代码将内存映射对齐到伙伴系统的最大分配阶,因为要使所有的计算都工作正常,这是必需的。

    指向该空间的指针不仅保存在pglist_data实例中,还保存在全局变量mem_map中,前提是当前考察的结点是系统的第0个结点(如果系统只有一个内存结点,则总是这样)。mem_map是一个全局数组,在讲解内存管理时,我们会经常遇到, 定义在mm/memory.c?v=4.7, line 85

    struct page *mem_map;

    然后在free_area_init_node函数的最后, 通过free_area_init_core来完成内存域zone的初始化

    5 free_area_init_core初始化内存域zone


    初始化内存域数据结构涉及的繁重工作由free_area_init_core执行,它会依次遍历结点的所有内存域, 该函数定义在mm/page_alloc.c?v=4.7, line 5932

    5.1 free_area_init_core函数代码注释


    /*
     * Set up the zone data structures:
     *   - mark all pages reserved
     *   - mark all memory queues empty
     *   - clear the memory bitmaps
     *
     * NOTE: pgdat should get zeroed by caller.
     */
    static void __paginginit free_area_init_core(struct pglist_data *pgdat)
    {
        enum zone_type j;
        int nid = pgdat->node_id;
        int ret;
    
        /*  初始化pgdat->node_size_lock自旋锁  */
        pgdat_resize_init(pgdat);
    #ifdef CONFIG_NUMA_BALANCING
        spin_lock_init(&pgdat->numabalancing_migrate_lock);
        pgdat->numabalancing_migrate_nr_pages = 0;
        pgdat->numabalancing_migrate_next_window = jiffies;
    #endif
    #ifdef CONFIG_TRANSPARENT_HUGEPAGE
        spin_lock_init(&pgdat->split_queue_lock);
        INIT_LIST_HEAD(&pgdat->split_queue);
        pgdat->split_queue_len = 0;
    #endif
    
        /*  初始化pgdat->kswapd_wait等待队列  */
        init_waitqueue_head(&pgdat->kswapd_wait);
        /*  初始化页换出守护进程创建空闲块的大小
         *  为2^kswapd_max_order  */
        init_waitqueue_head(&pgdat->pfmemalloc_wait);
    #ifdef CONFIG_COMPACTION
        init_waitqueue_head(&pgdat->kcompactd_wait);
    #endif
        pgdat_page_ext_init(pgdat);
    
        /* 遍历每个管理区 */
        for (j = 0; j < MAX_NR_ZONES; j++) {
            struct zone *zone = pgdat->node_zones + j;
            unsigned long size, realsize, freesize, memmap_pages;
            unsigned long zone_start_pfn = zone->zone_start_pfn;
    
            /*  size为该管理区中的页框数,包括洞 */
            size = zone->spanned_pages;
             /* realsize为管理区中的页框数,不包括洞  /
            realsize = freesize = zone->present_pages;
    
            /*
             * Adjust freesize so that it accounts for how much memory
             * is used by this zone for memmap. This affects the watermark
             * and per-cpu initialisations
             * 调整realsize的大小,即减去page结构体占用的内存大小  */
            /*  memmap_pags为包括洞的所有页框的page结构体所占的大小  */
            memmap_pages = calc_memmap_size(size, realsize);
            if (!is_highmem_idx(j)) {
                if (freesize >= memmap_pages) {
                    freesize -= memmap_pages;
                    if (memmap_pages)
                        printk(KERN_DEBUG
                               "  %s zone: %lu pages used for memmap\n",
                               zone_names[j], memmap_pages);
                } else  /*  内存不够存放page结构体  */
                    pr_warn("  %s zone: %lu pages exceeds freesize %lu\n",
                        zone_names[j], memmap_pages, freesize);
            }
    
            /* Account for reserved pages
             * 调整realsize的大小,即减去DMA保留页的大小  */
            if (j == 0 && freesize > dma_reserve) {
                freesize -= dma_reserve;
                printk(KERN_DEBUG "  %s zone: %lu pages reserved\n",
                        zone_names[0], dma_reserve);
            }
    
            if (!is_highmem_idx(j))
                nr_kernel_pages += freesize;
            /* Charge for highmem memmap if there are enough kernel pages */
            else if (nr_kernel_pages > memmap_pages * 2)
                nr_kernel_pages -= memmap_pages;
            nr_all_pages += freesize;
    
            /*
             * Set an approximate value for lowmem here, it will be adjusted
             * when the bootmem allocator frees pages into the buddy system.
             * And all highmem pages will be managed by the buddy system.
             */
            /* 设置zone->spanned_pages为包括洞的页框数  */
            zone->managed_pages = is_highmem_idx(j) ? realsize : freesize;
    #ifdef CONFIG_NUMA
            /* 设置zone中的节点标识符 */
            zone->node = nid;
            /* 设置可回收页面比率 */
            zone->min_unmapped_pages = (freesize*sysctl_min_unmapped_ratio)
                            / 100;
            /* 设置slab回收缓存页的比率 */
            zone->min_slab_pages = (freesize * sysctl_min_slab_ratio) / 100;
    #endif
            /*  设置zone的名称  */
            zone->name = zone_names[j];
    
            /* 初始化各种锁 */
            spin_lock_init(&zone->lock);
            spin_lock_init(&zone->lru_lock);
            zone_seqlock_init(zone);
            /* 设置管理区属于的节点对应的pg_data_t结构 */
            zone->zone_pgdat = pgdat;
            /* 初始化cpu的页面缓存 */
            zone_pcp_init(zone);
    
            /* For bootup, initialized properly in watermark setup */
            mod_zone_page_state(zone, NR_ALLOC_BATCH, zone->managed_pages);
    
            /* 初始化lru相关成员 */
            lruvec_init(&zone->lruvec);
            if (!size)
                continue;
    
            set_pageblock_order();
            /* 定义了CONFIG_SPARSEMEM该函数为空 */
            setup_usemap(pgdat, zone, zone_start_pfn, size);
            /* 设置pgdat->nr_zones和zone->zone_start_pfn成员
             * 初始化zone->free_area成员
             * 初始化zone->wait_table相关成员
             */
             ret = init_currently_empty_zone(zone, zone_start_pfn, size);
            BUG_ON(ret);
            /* 初始化该zone对应的page结构 */
            memmap_init(size, nid, j, zone_start_pfn);
        }
        /*  ......  */
    }

    5.2 流程讲解


    初始化内存域数据结构涉及的繁重工作由free_area_init_core执行,它会依次遍历结点的所有内存域

    static void __paginginit free_area_init_core(struct pglist_data *pgdat)
    {
        enum zone_type j;
        int nid = pgdat->node_id;
        int ret;
    
        /*  ......  */
        /* 遍历每个管理区 */
        for (j = 0; j < MAX_NR_ZONES; j++) {
            struct zone *zone = pgdat->node_zones + j;
            unsigned long size, realsize, freesize, memmap_pages;
            unsigned long zone_start_pfn = zone->zone_start_pfn;
    
            /*  size为该管理区中的页框数,包括洞 */
            size = zone->spanned_pages;
             /* realsize为管理区中的页框数,不包括洞  /
            realsize = freesize = zone->present_pages;
    
            /*  ......  */
    }

    内存域的真实长度,可通过跨越的页数减去空洞覆盖的页数而得到。这两个值是通过两个辅助函数计算的,我不会更详细地讨论了。其复杂性实质上取决于内存模型和所选定的配置选项,但所有变体最终都没有什么意外之处

    static void __paginginit free_area_init_core(struct pglist_data *pgdat)
    {
            /*  ......  */
            if (!is_highmem_idx(j))
                nr_kernel_pages += freesize;
            /* Charge for highmem memmap if there are enough kernel pages */
            else if (nr_kernel_pages > memmap_pages * 2)
                nr_kernel_pages -= memmap_pages;
            nr_all_pages += freesize;
    
            /*
             * Set an approximate value for lowmem here, it will be adjusted
             * when the bootmem allocator frees pages into the buddy system.
             * And all highmem pages will be managed by the buddy system.
             */
            /* 设置zone->spanned_pages为包括洞的页框数  */
            zone->managed_pages = is_highmem_idx(j) ? realsize : freesize;
    #ifdef CONFIG_NUMA
            /* 设置zone中的节点标识符 */
            zone->node = nid;
            /* 设置可回收页面比率 */
            zone->min_unmapped_pages = (freesize*sysctl_min_unmapped_ratio)
                            / 100;
            /* 设置slab回收缓存页的比率 */
            zone->min_slab_pages = (freesize * sysctl_min_slab_ratio) / 100;
    #endif
            /*  设置zone的名称  */
            zone->name = zone_names[j];
    
            /* 初始化各种锁 */
            spin_lock_init(&zone->lock);
            spin_lock_init(&zone->lru_lock);
            zone_seqlock_init(zone);
            /* 设置管理区属于的节点对应的pg_data_t结构 */
            zone->zone_pgdat = pgdat;
            /*  ......  */
    }

    内核使用两个全局变量跟踪系统中的页数。nr_kernel_pages统计所有一致映射的页,而nr_all_pages还包括高端内存页在内free_area_init_core始化为0

    我们比较感兴趣的是调用的两个辅助函数

    • zone_pcp_init尝试初始化该内存域的per-CPU缓存, 定义在mm/page_alloc.c?v=4.7, line 5443

    • init_currently_empty_zone初始化free_area列表,并将属于该内存域的所有page实例都设置为初始默认值。正如前文的讨论,调用了memmap_init_zone来初始化内存域的页, 定义在mm/page_alloc.c?v=4.7, line 5458

    我们还可以回想前文提到的,所有页属性起初都设置MIGRATE_MOVABLE。
    此外,空闲列表是在zone_init_free_lists中初始化的

    static void __paginginit free_area_init_core(struct pglist_data *pgdat)
    {
        /*  ......  */
        {
            /* 初始化cpu的页面缓存 */
            zone_pcp_init(zone);
    
            /* 设置pgdat->nr_zones和zone->zone_start_pfn成员
             * 初始化zone->free_area成员
             * 初始化zone->wait_table相关成员
             */
             ret = init_currently_empty_zone(zone, zone_start_pfn, size);
            BUG_ON(ret);
            /* 初始化该zone对应的page结构 */
            memmap_init(size, nid, j, zone_start_pfn);
        }
        /*  ......  */
    }

    6 memmap_init初始化page页面


    在free_area_init_core初始化内存管理区zone的过程中, 通过memmap_init函数对每个内存管理区zone的page内存进行了初始化

    memmap_init函数定义在mm/page_alloc.c?v=4.7, line

    #ifndef __HAVE_ARCH_MEMMAP_INIT
    #define memmap_init(size, nid, zone, start_pfn) \
        memmap_init_zone((size), (nid), (zone), (start_pfn), MEMMAP_EARLY)
    #endif

    memmap_init_zone函数完成了page的初始化工作, 该函数定义在mm/page_alloc.c?v=4.7, line 5139

    至此,节点和管理区的关键数据已完成初始化,内核在后面为内存管理做得一个准备工作就是将所有节点的管理区都链入到zonelist中,便于后面内存分配工作的进行

    内核在start_kernel()–>build_all_zonelist()中完成zonelist的初始化

    7 总结


    7.1 start_kernel启动流程


    start_kernel()
        |---->page_address_init()
        |     考虑支持高端内存
        |     业务:初始化page_address_pool链表;
        |          将page_address_maps数组元素按索引降序插入
        |          page_address_pool链表; 
        |          初始化page_address_htable数组.
        | 
        |---->setup_arch(&command_line);
        |
        |---->setup_per_cpu_areas();
        |     为per-CPU变量分配空间
        |
        |---->build_all_zonelist()
        |     为系统中的zone建立后备zone的列表.
        |     所有zone的后备列表都在
        |     pglist_data->node_zonelists[0]中;
        |
        |     期间也对per-CPU变量boot_pageset做了初始化. 
        |
        |---->page_alloc_init()
             |---->hotcpu_notifier(page_alloc_cpu_notifier, 0);
             |     不考虑热插拔CPU 
             |
        |---->pidhash_init()
        |     详见下文.
        |     根据低端内存页数和散列度,分配hash空间,并赋予pid_hash
        |
        |---->vfs_caches_init_early()
              |---->dcache_init_early()
              |     dentry_hashtable空间,d_hash_shift, h_hash_mask赋值;
              |     同pidhash_init();
              |     区别:
              |         散列度变化了(13 - PAGE_SHIFT);
              |         传入alloc_large_system_hash的最后参数值为0;
              |
              |---->inode_init_early()
              |     inode_hashtable空间,i_hash_shift, i_hash_mask赋值;
              |     同pidhash_init();
              |     区别:
              |         散列度变化了(14 - PAGE_SHIFT);
              |         传入alloc_large_system_hash的最后参数值为0;
              |

    7.2 pidhash_init配置高端内存


    void pidhash_init(void)
        |---->pid_hash = alloc_large_system_hash("PID", sizeof(*pid_hash), 
        |         0, 18, HASH_EARLY|HASH_SMALL, &pidhash_shift, NULL, 4096);
        |     根据nr_kernel_pages(低端内存的页数),分配哈希数组,以及各个哈希
        |     数组元素下的哈希链表的空间,原理如下:
        |     number = nr_kernel_pages; 
        |     number >= (18 - PAGE_SHIFT) 根据散列度获得数组元素个数
        |     number = roundup_pow_of_two(number);
        |     pidhash_shift = max{x | 2**x <= number}
        |     size = number * sizeof(*pid_hash);
        |     使用位图分配器分配size空间,将返回值付给pid_hash;
        |
        |---->pidhash_size = 1 << pidhash_shift;
        |
        |---->for(i = 0; i < pidhash_size; i++)
        |         INIT_HLIST_HEAD(&pid_hash[i]);

    7.3 build_all_zonelists初始化每个内存节点的zonelists


    void build_all_zonelists(void)
        |---->set_zonelist_order()
             |---->current_zonelist_order = ZONELIST_ORDER_ZONE;
        |
        |---->__build_all_zonelists(NULL);
        |    Memory不支持热插拔, 为每个zone建立后备的zone,
        |    每个zone及自己后备的zone,形成zonelist
            |
            |---->pg_data_t *pgdat = NULL;
            |     pgdat = &contig_page_data;(单node)
            |
            |---->build_zonelists(pgdat);
            |     为每个zone建立后备zone的列表
                |
                |---->struct zonelist *zonelist = NULL;
                |     enum zone_type j;
                |     zonelist = &pgdat->node_zonelists[0];
                |
                |---->j = build_zonelists_node(pddat, zonelist, 0, MAX_NR_ZONES - 1);
                |     为pgdat->node_zones[0]建立后备的zone,node_zones[0]后备的zone
                |     存储在node_zonelist[0]内,对于node_zone[0]的后备zone,其后备的zone
                |     链表如下(只考虑UMA体系,而且不考虑ZONE_DMA):
                |     node_zonelist[0]._zonerefs[0].zone = &node_zones[2];
                |     node_zonelist[0]._zonerefs[0].zone_idx = 2;
                |     node_zonelist[0]._zonerefs[1].zone = &node_zones[1];
                |     node_zonelist[0]._zonerefs[1].zone_idx = 1;
                |     node_zonelist[0]._zonerefs[2].zone = &node_zones[0];
                |     node_zonelist[0]._zonerefs[2].zone_idx = 0;
                |
                |     zonelist->_zonerefs[3].zone = NULL;
                |     zonelist->_zonerefs[3].zone_idx = 0;
            |
            |---->build_zonelist_cache(pgdat);
                  |---->pdat->node_zonelists[0].zlcache_ptr = NULL;
                  |     UMA体系结构
                  |
            |---->for_each_possible_cpu(cpu)
            |     setup_pageset(&per_cpu(boot_pageset, cpu), 0);
                  |详见下文
        |---->vm_total_pages = nr_free_pagecache_pages();
        |    业务:获得所有zone中的present_pages总和.
        |
        |---->page_group_by_mobility_disabled = 0;
        |     对于代码中的判断条件一般不会成立,因为页数会最够多(内存较大)
    展开全文
  • java并发编程synchronized+volatile单例模式实例(安全初始化模式实例)

    安全发布对象的安全性都来自于JMM提供的保证,而造成不正确的发布原因,就是在”发布一个共享对象”与”另一个线程访问该对象”之间缺少一种Happen-Before排序.

    不安全的发布

    package safe_unsafe_publication;
    /**
     * Created by fang on 2017/11/24.
     * 不安全发布,一个线程正在初始化,另一个线程可能读到未构造完全的对象.
     */
    public class UnsafeLazyInitialization {
        private static Resource resource;
    
        public static Resource getInstance(){
            if(resource == null){
                resource = new Resource();
            }
            return resource;
        }
    }
    

    上述代码中的不安全性有两个方面
    1 资源竞争问题
    类的静态变量存在方法区,resource类变量(“地址变量”),被线程A 和线程B共享,但是当线程A写入resource地址变量值的时候,可能在cpu寄存器(cpu高速缓存)还未同步到cpu主存中(每一个线程都有自己的cpu高速缓存,在高速缓存中互相看不到).

    导致线程B在判断resource仍然为null,线程B可能也会new Resource()操作. 这就导致了资源的竞争,也就是(查看–修改–写入)并非是原子性操作.

    2 类的实例对象构造不完整,或构造错误问题.
    线程A在实行getInstance()方法的时候,发现resource为null,然后new Resource(),然后设置为指向resource.
    然后线程B在判断resource是否为null时,假设此时线程A中的类变量resource已经同步到CPU主存,B发现类变量resource并不为null,然后就获取resource地址,直接使用线程A new出来的对象.
    .B可能使用A创建的Resource对象并不完整,或者创建时错误的.(线程B看到A线程的执行顺序,和A真正的执行顺序可能不同,JVM重排列).

    (以上是自己的理解,与类的加载过程有关系,在加载 连接阶段已经有了类的地址变量,但下一个阶段才是对象的初始化阶段. 也就是在方法区已经存在对象实例的地址对象变量,但是堆中的实例对象并没有初始化完整.)

    官方说明:

    除了不可变对象外,使用被另一个线程初始化的对象通常是不安全的,除非对象的发布操作是在使用该对象的线程开始使用之前执行.

    安全发布

    安全初始化模式

    如下代码

     package safe_unsafe_publication;
    
    
     /**
      * Created by fang on 2017/11/24.
      * 安全的延迟加载
      */
     public class SafeLazyInitialization {
         private static Resource resource;
    
         public synchronized static Resource getInstance(){
             if(resource ==null){
                 resource = new Resource();
             }
             return resource;
         }
     }
    

    在类的方法上添加同步锁,synchronized一方面解决了非原子性问题,又解决了可见性问题,并且不会引发JVM重排列,解决了第一个例子中的两个问题.
    但是多线程每次都要调用这个方法,得到对象的引用,当访问量十分大的时候,每次调用这个方法获得锁和释放锁的开销似乎是不必要的: 一旦初始化完成获得锁和释放锁就显得没有那么必要,可以用提前初始化方式进行优化.

    提前初始化

    代码如下,可以定义静态成员变量,直接初始化Resource类.

    
       /**
        * Created by fang on 2017/11/24.
        * 提前初始化方式.
        */
       public class EagerInitialzation {
           private static Resource resource = new Resource();
    
           public static Resource getResource(){
               return resource;
           }
       }

    避免了每次调用getInstance时所产生的同步开销.
    通过这项技术和JVM的延迟加载技术结合起来,可以形成一种延迟初始化技术, 这样在代码中就不需要同步.(与类的加载有关,当类有static修饰时,读取静态字段的时候,虚拟机会对类进行”初始化”,且JVM保证了静态类初始化且赋值的时候的原子性和可见性,所以无需我们显示的同步.)

    采用特殊的初始化方式来处理静态域,并提供了额外的线程安全保证. 静态初始化器是由JVM在类的初始化阶段执行,即类加载后并且被线程使用之前。
    JVM在初始化期间获得一个锁,并且每个线程都至少获取一个这个锁,以确保这个类已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见.
    因此无论是构造期还是被引用时,静态初始化对象都不需要显示的同步.

    这个规则仅仅适用于构造时状态,如果对象是可变的,那么在读取线程和线程指尖仍然需要通过同步来确保修改操作是可见的,以及避免数据被破坏.

    延迟初始化占位

    package safe_unsafe_publication;
    
    /**
     * Created by fang on 2017/11/24.
     *  延长初始化占位符.
     */
    public class ResourceFactory {
        public static class ResourceHolder{
            public static Resource resource = new Resource();
        }
    
        public static Resource getResource(){
            return ResourceHolder.resource;
        }
    }
    

    上个例子中的提前初始化,可能在加载对象EagerInitialzation时,就会立刻初始化static变量修饰的Resource.我们想要做到在使用它时再进行初始化.
    使用专门的ResourceFactory类初始化Resource.JVM推迟ResourceHolder初始化操作,直到使用这个类才初始化.(https://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4.1:初始化发生情况)
    当执行getResource()方法时,都会使ResourceHolder被加载和被初始化,此时静态初始化器将会执行Resource的初始化操作.

    双重检查锁初始化(DCL)

    
       /**
        * Created by fang on 2017/11/25.
        * 双重检查锁初始化DCL
        */
       public class DoubleCheckedLocking {
    
           private static Resource resource;
    
           public static Resource getInstance(){
               if(resource == null){
                   synchronized (DoubleCheckedLocking.class){
                       if(resource == null){
                           resource = new Resource();
                       }
                   }
               }
               return resource;
           }
       }

    和安全初始化模式比,在原来的基础上,将synchronized在方法上改为同步到DoubleCheckLocking这个类上.
    避免了无论Resource是否被实例化都要获取锁和释放锁操作,这样只有resource == null时才获取这个锁,并进行同步判断.
    在资源竞争方面线程是安全的,但是在可见性方面或者说类的构造是否完整方面,就会有 例子”不安全发布中的问题2”,当这个类的类变量(“地址变量”)已经存在但new出来的时候未完全加载成功的时候,另一个线程可能就会使用这个错误的对象或者是无效的对象(与类的加载机制有关,区分实例化一个类和实例化一个对象.)

    怎么解决类的不完全构造和jvm重排列问题呢?
    如下代码,在定义的时候添加上volatile关键字这会解决这个上述问题,因为volatile变量,控制了JVM的重排列,并且两个线程都可以在内存中可见.

    (volatile关键字:http://blog.csdn.net/lovesummerforever/article码,在定义的时候添加上volatile关键字这会解决这个上述问题,因为volatile变量(volatile关键字:http://blog.csdn.net/lovesummerforever/article/details/78603856)

    package safe_unsafe_publication;
    
    /**
     * Created by fang on 2017/11/25.
     * 双重检查锁初始化DCL
     */
    public class DoubleCheckedLocking {
    
        private static volatile Resource resource;
    
        public static Resource getInstance(){
            if(resource == null){
                synchronized (DoubleCheckedLocking.class){
                    if(resource == null){
                        resource = new Resource();
                    }
                }
            }
            return resource;
        }
    }
    

    初始化过程中的安全性

    如果Resource是不可变的,或者我们在使用过程中不需要改变Resource,我们可以用final来修饰,确保了Resource的原子性和可见性.

    在java内存模型中,final域能确保初始化过程的安全性,从而可以不受限制的访问不可变对象,并共享这些对象的时候无需同步.
    如下代码

    
       import java.util.HashMap;
       import java.util.Map;
    
       /**
        * Created by fang on 2017/11/26.
        * final保证可见性和原子性.
        */
       public class SafeStates {
           private final Map<String,String> states;
    
           public SafeStates(){
               states = new HashMap<String, String>();
               states.put("akasja","AK");
               states.put("alabama","AL");//and so on
           }
    
           public String getAbbreviation(String s){
               return states.get(s);
           }
       }

    对于final类的对象,初始化安全性可以防止对对象的初始引用被重排列到构造过程之前. 当构造函数完成时,构造函数对final域的所有写入操作,以及对通过这些域可以到达的任何变量的写入操作,都将被”冻结”, 并且任何获得该对象引用的线程都至少能确保看到被冻结的值.
    对于final域可到达的初始变量的写入操作,将不会与构造过程的操作一起被重排序.

    初始化安全性只能保证通过final域可达的值从构造过程完成时开始的可见性. 对于通过非final域可达的值,或者在构造完成后可能改变的值, 必须通过同步来确保可见性.

    summay

    对线程的可见性和原子性有了一定的了解,对非安全发布和安全发布有一些自己的理解,目前理解为这个程度,可能以后回来看会有新的理解.
    上述的一些观点和理解有问题,欢迎交流~
    这篇文章涉及到了JVM类的加载和java内存模型以及JVM的重排列,下面文章一一解释.

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

    千次阅读 2017-05-25 09:53:21
    成员初始化顺序属性、方法、构造方法和自由块都是类中的成员,在创建类的对象时,类中各成员的执行顺序: 父类静态成员和静态初始化快,按在代码中出现的顺序依次执行。 子类静态成员和静态初始化块,按在代码中出现...
  • 日期 内核版本 架构 作者 GitHub ... Linux内存管理在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到
  • DDR3内存详解,存储器结构+时序+初始化过程 2017-06-17 16:10:33a_chinese_man阅读数 23423更多 分类专栏:硬件开发基础 转自:http://www.360doc.com/content/14/0116/16/15528092_345730642.shtml 首先,...
  • 本系列旨在讲述从引导到完全建立内存管理体系过程中,内核对内存管理所经历的几种状态.阅读本系列前,建议先阅读memblock的相关文章.一些讲在前面的话在很久很久以前,linux内核还是支持直接从磁盘直接启动,也就是内核...
  • uboot初始化中,为何要设置CPU为SVC模式而不是设置为其他模式 在看Uboot的start.S文件时候,发现其最开始初始化系统,做的第一件事情,就是将CPU设置为SVC模式,但是S3C2440的CPU的core是ARM920T,其有7种模式,...
  • DMA初始化设置

    千次阅读 2018-08-21 09:25:10
    数据传递可以从适配卡到内存,从内存到适配卡或从一段内存到另一段内存。   DMA技术的重要性在于,利用它进行数据传送时不需要CPU的参与。每台电脑主机板上都有DMA控制器,通常计算机对其编程,并用一个适配器上...
  • RapidIO的启动与初始化

    千次阅读 2019-05-16 14:59:50
    RapidIO的启动与初始化 概述 RapidIO的启动与初始化主要包括以下几个方面的内容: 系统初始化 器件枚举 路由表配置 存储器映射 一旦配置好RapidIO系统,系统就会在RapidIO互连结构中透明地传递I/O事务。从软件...
  • 内存是通过指针寻址的,因而CPU的字长决定了CPU所能管理的地址空间的大小,该地址空间就被称为虚拟地址空间,因此32位CPU的虚拟地址空间大小为4G,这和实际的物理内存数量无关。 Linux内核将虚拟地址空间分成了两...
  • 解释器截取new这个关键字时,就会为ClassName量身定做一个内存空间,这个时候也就是为该类中的所有成员变量分配内存空间之时,然后按照前面的顺序进行初始化,所有引用类型将其制成null 基本数据类型为0;...
  • 第五章 初始化与清理

    千次阅读 多人点赞 2020-10-09 14:38:59
    文章目录1.用构造器确保初始化2.方法重载3.默认构造器(无参构造器)4.this关键字static...在操作对象前,已为对象分配内存空间,并调用相应构造器对象已被初始化了 2.方法重载 构造器/方法名相同,形式参数不同
  • 修改数据库初始化参数

    千次阅读 2018-08-26 20:58:54
    修改数据库初始化参数 初始化参数文件 启动实例时,会读取初始化参数文件。参数文件有两种类型。 用参数来约束进程和内存 数据库启动,先启动实例,在找database,用实例操作数据库中的内容 服务器参数文件(spfile...
  • 以下是我要讲解的GPIO初始化程序段,对时钟进行使能,对引脚进行模式配置,速度设定,尽量讲解小白学习过程中不解的每一个方面。 代码讲解时我是根据一个程序边讲边跳入它的声明或是定义中讲解的。
  • 初始化参数文件

    千次阅读 2013-10-02 16:03:18
    1、初始化参数文件   启动实例时会读取初始化参数文件。参数文件有两种类型。   • 服务器参数文件(SPFILE): 二进制文件,由oracle server维护,rman可以备份。 创建spfile create spfile[=‘路径’ ]from...
  • SD卡初始化步骤详解

    千次阅读 2016-12-15 14:55:29
    7、初始化成功后,将spi切换为高速模式,我这里用的是4m。 至此,SD卡初始化过程就结束了,记得几点:在发送cmd函数后,记得补偿时钟;在读寄存器位时,需要select拉低;初始化时,一定要是低速模式并且有...
  • SD卡初始化步骤总结

    千次阅读 2016-12-15 15:56:46
    7、初始化成功后,将spi切换为高速模式,我这里用的是4m。 至此,SD卡初始化过程就结束了,记得几点:在发送cmd函数后,记得补偿时钟;在读寄存器位时,需要select拉低;初始化时,一定要是低速模式并且有...
  • Android 内存泄漏总结(超级实用)

    万次阅读 2016-06-22 16:45:25
    Android 内存泄漏总结内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,就是该被释放的对象没有释放,一直被某个或某些实例所持有却不再被使用...
  • MPC5125初始化DDR2 SDRAM那些事儿

    千次阅读 2012-04-17 15:44:28
    产品升级将DDR一代的SDRAM换成DDR2代的SDRAM,在核心频率都为200MHZ的...(1)对于MPC5125 DDR控制的初始化:  MPC5125 DDR控制器的特点:  The DRAM controller is a multi-port controller that listens to in
  • LINUX系统初始化

    万次阅读 2018-04-07 20:35:44
    这个进程就是著名的pid为1的init进程(内核态的),它会继续完成剩下的初始化工作比且创建若干个用于高速缓存和虚拟主存管理的内核线程,如kswapd和bdflush等,然后execve(/sbin/init)(生成用户态的init进程pid=1,...
  • STM32 HAL时钟初始化

    千次阅读 2019-04-08 10:50:54
    /* 初始化并同时设置FLASH 的延迟周期 */ if ( HAL_RCC_ClockConfig ( & RCC_ClkInitStruct , FLASH_LATENCY_2 ) != HAL_OK ) { Error_Handler ( ) ; } 1、LSI RC,低速内部时钟(RC振荡器)...
  • Linux内核初始化步骤(一)

    千次阅读 2018-11-25 23:33:45
    内核的初始化过程由start_kernel函数(\init\main.c)开始,至第一个用户进程init结束,调用了一系列的初始化函数对所有的内核组件进行初始化。其中,start_kernel、rest_init、kernel_init、init_post4个函数构成了...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 38,060
精华内容 15,224
关键字:

内存初始化模式超高速