Skip to content

Latest commit

 

History

History
1288 lines (1026 loc) · 76.6 KB

File metadata and controls

1288 lines (1026 loc) · 76.6 KB

四、内存管理和分配器

内存管理的效率在很大程度上决定了整个内核的效率。 随意管理的内存系统可能会严重影响其他子系统的性能,使内存成为内核的关键组件。 这个子系统通过虚拟化物理内存并管理它们发起的所有动态分配请求来启动所有进程和内核服务。 在维持操作效率和优化资源方面,内存子系统还可以处理广泛的操作。 操作既是特定于体系结构的,又是独立的,这要求总体设计和实现是公正和可调整的。 为了理解这个庞大的子系统,我们将在本章中仔细研究以下几个方面:

  • 物理内存表示法
  • 节点和区域的概念
  • 页面分配器
  • 搭伴制
  • Kmalloc 津贴
  • 片缓存
  • Vmalloc 津贴
  • 连续内存分配

初始化操作

在大多数架构中,在*复位时,*处理器以正常或物理地址模式(在 x86 中也称为实模式)初始化,并开始执行在复位矢量处找到的平台固件指令。 这些固件指令(可以是单个二进制或多级二进制)被编程以执行各种操作,其中包括存储器控制器的初始化、物理 RAM 的校准以及将二进制内核映像加载到物理存储器的特定区域等。

在实模式下,处理器不支持虚拟寻址,Linux 是为具有保护模式的系统设计和实现的,它需要虚拟寻址来启用进程保护和隔离,这是内核提供的一个重要抽象(回想一下第 1 章理解进程、地址空间和线程)。 这要求处理器在内核启动并开始引导操作和子系统初始化之前切换到保护模式并打开虚拟地址支持。 切换到保护模式需要在启用分页的过程中通过设置适当的核心数据结构来初始化 MMU 芯片组。 这些操作是特定于体系结构的,并在内核源代码树的ARCH分支中实现。 在内核构建期间,这些源代码被编译并作为头文件链接到保护模式内核映像;这个头文件称为内核引导程序实模式内核

下面是 x86 体系结构的引导程序的main()例程;该函数在实模式下执行,负责在步入保护模式之前通过调用go_to_protected_mode()来分配适当的资源:

/* arch/x86/boot/main.c */
void main(void)
{
 /* First, copy the boot header into the "zeropage" */
 copy_boot_params();

 /* Initialize the early-boot console */
 console_init();
 if (cmdline_find_option_bool("debug"))
 puts("early console in setup coden");

 /* End of heap check */
 init_heap();

 /* Make sure we have all the proper CPU support */
 if (validate_cpu()) {
 puts("Unable to boot - please use a kernel appropriate "
 "for your CPU.n");
 die();
 }

 /* Tell the BIOS what CPU mode we intend to run in. */
 set_bios_mode();

 /* Detect memory layout */
 detect_memory();

 /* Set keyboard repeat rate (why?) and query the lock flags */
 keyboard_init();

 /* Query Intel SpeedStep (IST) information */
 query_ist();

 /* Query APM information */
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
 query_apm_bios();
#endif

 /* Query EDD information */
#if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)
 query_edd();
#endif

 /* Set the video mode */
 set_video();

 /* Do the last things and invoke protected mode */
 go_to_protected_mode();
}

为设置 MMU 和处理到保护模式的转换而调用的实模式内核例程是特定于体系结构的(我们在这里不会涉及这些例程)。 无论使用哪种特定于体系结构的代码,主要目标都是通过打开分页来启用对虚拟寻址的支持。 启用分页后,系统开始将物理内存(RAM)视为一组固定大小的块,称为页帧。 通过对 MMU 的分页单元进行适当编程来配置页框的大小;大多数 MMU 支持 4k、8k、16k、64k 至 4MB 的帧大小配置选项。 然而,大多数体系结构的 Linux 内核的默认构建配置选择 4k 作为其标准页帧大小。

页面描述符

页帧是可能的最小内存分配单位,内核需要利用它们来满足其所有内存需求。 将物理内存映射到用户模式进程的虚拟地址空间需要一些页帧,一些页帧用于内核代码及其数据结构,还有一些页帧用于处理进程或内核服务提出的动态分配请求。 为了有效地管理这些操作,内核需要区分当前在使用中的页帧和那些空闲和可用的页帧。 这一目的是通过称为struct page的独立于体系结构的数据结构来实现的,该结构被定义为保存与页帧有关的所有元数据,包括其当前状态。 为找到的每个物理页帧分配一个struct page实例,内核必须始终在主内存中维护一个页面实例列表。

页面结构是内核中使用最多的数据结构之一,它被各种内核代码路径引用。 该结构填充了各种元素,这些元素的相关性完全基于物理框架的状态。 例如,页面结构的特定成员指定是否将相应的物理页面映射到进程或一组进程的虚拟地址空间。 当物理页已保留用于动态分配时,这些字段被认为是无效的。 为了确保内存中的页面实例仅与相关字段一起分配,大量使用联合来填充成员字段。 这是一个谨慎的选择,因为它可以在不增加内存大小的情况下将更多信息塞进页面结构:

/*include/linux/mm-types.h */ 
/* The objects in struct page are organized in double word blocks in
 * order to allows us to use atomic double word operations on portions
 * of struct page. That is currently only used by slub but the arrangement
 * allows the use of atomic double word operations on the flags/mapping
 * and lru list pointers also.
 */
struct page {
        /* First double word block */
         unsigned long flags; /* Atomic flags, some possibly updated asynchronously */   union {
          struct address_space *mapping; 
          void *s_mem; /* slab first object */
          atomic_t compound_mapcount; /* first tail page */
          /* page_deferred_list().next -- second tail page */
   };
  ....
  ....

}

以下是对页面结构的重要成员的简要描述。 请注意,这里的许多细节都假设您熟悉内存子系统的其他方面,我们将在本章的后续部分讨论这些方面,例如内存分配器、页表等等。 我建议新读者在熟悉必要的前提条件后跳过这一节并重新阅读。

标出 / 悬旗于 / 标记

这是一个unsigned long位字段,它保存描述物理页状态的标志。 标志常量通过内核头include/linux/page-flags.h中的enum定义。 下表列出了重要的标志常量:

| 标志 | 说明 | | PG_locked | 用于指示页面是否锁定;此位在启动页面上的 I/O 操作时设置,并在完成时清除。 | | PG_error | 用于指示错误页。 在页面上发生 I/O 错误时设置。 | | PG_referenced | 设置为指示页缓存的页回收。 | | PG_uptodate | 设置以指示从磁盘执行读取操作后页面是否有效。 | | PG_dirty | 当文件备份页被修改并且与其磁盘映像不同步时设置。 | | PG_lru | 用于指示设置了最近最少使用的位,这有助于处理页面回收。 | | PG_active | 用于指示页面是否在活动列表中。 | | PG_slab | 用于指示该页由片分配器管理。 | | PG_reserved | 用于指示不可交换的保留页面。 | | PG_private | 用于指示该页由文件系统用来保存其私有数据。 | | PG_writeback | 在文件备份页上开始写回操作时设置 | | PG_head | 用于指示复合页面的标题页。 | | PG_swapcache | 用于指示页面是否在交换缓存中。 | | PG_mappedtodisk | 用于指示页面映射到存储上的。 | | PG_swapbacked | 页面由交换支持。 | | PG_unevictable | 用于指示页面在不可收回列表中;通常,此位是为 ramfs 和SHM_LOCKed共享内存页面拥有的页面设置的。 | | PG_mlocked | 用于指示页面上启用了 VMA 锁定。 |

对于checksetclear个单独的页位,存在许多宏;这些操作保证为atomic,并在内核标题/include/linux/page-flags.h中声明。 它们被调用来操作来自各种内核代码路径的页面标志:

/*Macros to create function definitions for page flags */
#define TESTPAGEFLAG(uname, lname, policy) \
static __always_inline int Page##uname(struct page *page) \
{ return test_bit(PG_##lname, &policy(page, 0)->flags); }

#define SETPAGEFLAG(uname, lname, policy) \
static __always_inline void SetPage##uname(struct page *page) \
{ set_bit(PG_##lname, &policy(page, 1)->flags); }

#define CLEARPAGEFLAG(uname, lname, policy) \
static __always_inline void ClearPage##uname(struct page *page) \
{ clear_bit(PG_##lname, &policy(page, 1)->flags); }

#define __SETPAGEFLAG(uname, lname, policy) \
static __always_inline void __SetPage##uname(struct page *page) \
{ __set_bit(PG_##lname, &policy(page, 1)->flags); }

#define __CLEARPAGEFLAG(uname, lname, policy) \
static __always_inline void __ClearPage##uname(struct page *page) \
{ __clear_bit(PG_##lname, &policy(page, 1)->flags); }

#define TESTSETFLAG(uname, lname, policy) \
static __always_inline int TestSetPage##uname(struct page *page) \
{ return test_and_set_bit(PG_##lname, &policy(page, 1)->flags); }

#define TESTCLEARFLAG(uname, lname, policy) \
static __always_inline int TestClearPage##uname(struct page *page) \
{ return test_and_clear_bit(PG_##lname, &policy(page, 1)->flags); }

*....
....* 

映射 / 映现

页面描述符的另一个重要元素是类型struct address_space的指针*mapping。 然而,*,*这是一个棘手的指针,它可能指向struct address_space的实例,也可能指向struct anon_vma的实例。 在我们详细介绍如何实现这一点之前,让我们先了解这些结构及其所代表的资源的重要性。

文件系统使用空闲页面(来自页面缓存)来缓存最近访问的磁盘文件的数据。 此机制有助于最大限度地减少磁盘 I/O 操作:当缓存中的文件数据被修改时,通过设置PG_dirty位将适当的页标记为脏;通过按策略间隔安排磁盘 I/O,所有脏页都被写入相应的磁盘块。 struct address_space是一个抽象概念,表示一组用于文件缓存的页面。 页面高速缓存的空闲页面也可以映射到进程或进程组以进行动态分配,为此类分配映射的页面称为匿名页面映射。 struct anon_vma的实例表示使用匿名页创建的内存块,这些匿名页映射到一个或多个进程的虚拟地址空间(通过 VMA 实例)。

使用指向任一数据结构的地址的指针的棘手动态初始化是通过位操作实现的。 如果指针*mapping的低位被清除,则表示页面被映射到inode,并且指针指向struct address_space。 如果设置了低位,则表示匿名映射,这意味着指针指向struct anon_vma的实例。 这是通过确保分配与sizeof(long)对齐的address_space实例来实现的,这使得指向address_space的指针的最低有效位被取消设置(即设置为 0)。

区域和节点

对于整个内存管理框架而言,基本的主要数据结构是区域节点。 让我们熟悉一下这些数据结构背后的核心概念。

内存区

为了有效地管理内存分配,物理页面被组织成称为区域的组。 每个区域中的页面用于特定需求,如 DMA、高内存和其他常规分配需求。 内核头mmzone.h中的enum声明了区域常量:

/* include/linux/mmzone.h */
enum zone_type {
#ifdef CONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
 ZONE_DMA32,
#endif
#ifdef CONFIG_HIGHMEM
 ZONE_HIGHMEM,
#endif
 ZONE_MOVABLE,
#ifdef CONFIG_ZONE_DEVICE
 ZONE_DEVICE,
#endif
 __MAX_NR_ZONES
};

ZONE_DMA: 此区域中的页面保留给无法在所有可寻址存储器上启动 DMA 的设备。 此区域的大小取决于体系结构:

| 建筑艺术 / 建筑业 / 建筑风格 / 建筑学 | 界限 / 限制 / 范围 / 限度 | | PARIC,ia64,SPARC | <4G | | S390 | <2G | | 手臂 / 像手臂的东西 / 部门 / 武器 | 易变的 / 可变的 / 方向不定的 / 变量的 | | 希腊字母表中第一个字母 / 开端 | 无限制或小于 16MB | | 帖子主题:Re:Колибри | <16MB |

ZONE_DMA32:此用于支持可在<4G 内存上执行 DMA 的 32 位设备。 此区域仅在 x86-64 平台上存在。

ZONE_NORMAL:所有可寻址内存都被认为是正常的。 只要 DMA 设备支持所有可寻址存储器,就可以在这些页面上启动 DMA 操作。

ZONE_HIGHMEM:这个区域包含只有内核通过显式映射到其地址空间才能访问的页面;换句话说,内核段之外的所有物理内存页面都落入这个区域。 此区域仅适用于具有 3:1 虚拟地址分割的 32 位平台(3G 用于用户模式,1G 地址空间用于内核);例如,在 i386 上,允许内核寻址超过 900MB 的内存将需要为内核需要访问的每个页面设置特殊映射(页表条目)。

ZONE_MOVABLE:内存碎片是现代操作系统面临的挑战之一,Linux 也不例外。 从内核启动的那一刻起,在其整个运行时,都会为一组任务分配和释放页面,从而产生具有物理上连续页面的小内存区域。 考虑到 Linux 对虚拟寻址的支持,碎片可能不会成为各种进程顺利执行的障碍,因为物理上分散的内存总是可以通过页表映射到几乎连续的地址空间。 然而,也有一些场景,比如 DMA 分配和为内核数据结构设置缓存,这些场景对物理上连续的区域有着严格的需求。

多年来,内核开发人员一直在发展许多反碎片技术来缓解碎片。 引入ZONE_MOVABLE就是这些尝试之一。 这里的核心思想是跟踪每个区域中的个可移动的页面,并在这个伪区域下表示它们,这有助于防止碎片(我们将在伙伴系统的下一节中详细讨论这一点)。

区域的大小将在引导时通过内核参数kernelcore之一进行配置;请注意,分配的值指定了认为不可移动、和其余的可移动的内存量。 一般来说,内存管理器被配置为考虑将页面从最高填充的区域迁移到ZONE_MOVABLE,对于 x86 32 位计算机,可能是ZONE_HIGHMEM,对于 x86_64,可能是ZONE_DMA32

ZONE_DEVICE:此区域已划分为支持热插拔内存,如大容量永久内存阵列永久存储器在许多方面与 DRAM 非常相似;具体地说,CPU 可以直接在字节级别对它们进行寻址。 但是,持久性、性能(写入速度较慢)和大小(通常以 TB 为单位)等特征将它们与普通内存区分开来。 要让内核支持这样的 4KB 页面大小的内存,它需要枚举数十亿个页面结构,这将占用相当大百分比的主内存,或者根本不适合。 因此,内核开发人员选择将持久内存视为设备,而不是类似于内存;这意味着内核可以依靠适当的驱动程序来管理此类内存。

void *devm_memremap_pages(struct device *dev, struct resource *res,
                        struct percpu_ref *ref, struct vmem_altmap *altmap); 

永久内存驱动程序的devm_memremap_pages()例程将永久内存区域映射到内核的地址空间,并在永久设备内存中设置相关的页面结构。 这些映射下的所有页面都分组在ZONE_DEVICE下。 具有不同的来标记这些页允许存储器管理器将它们与常规的统一存储器页区分开来。

内存节点

Linux 内核在很长一段时间内都是为了支持多处理器机器体系结构而实现的。 内核实现各种资源,如每 CPU 数据缓存、互斥锁和原子操作宏,这些资源可跨各种 SMP 感知子系统(如进程调度器和设备管理等)使用。 特别是,内存管理子系统的角色对于内核在这样的架构上运行是至关重要的,因为它需要按照每个处理器的观点来虚拟化内存。 根据每个处理器的感知和对系统上内存的访问延迟,多处理器机器架构大致分为两类。

**统一内存访问架构(Uniform Memory Access Architecture,UMA):**这些是多处理器架构机器,处理器通过互连连接在一起,并共享物理内存和 I/O 端口。 由于内存访问延迟,它们被命名为 UMA 系统,无论它们是从哪个处理器启动的,该延迟都是统一和固定的。 大多数对称多处理器系统都是 UMA。

非统一内存访问体系结构(Non-Uniform Memory Access Architecture,NUMA):这些多处理器机器的设计与 UMA形成了鲜明对比。 这些系统为每个具有固定时间访问延迟的处理器设计了专用内存。 然而,处理器可以通过适当的互连来启动对其他处理器的本地存储器的访问操作,并且这种操作呈现可变的时间访问等待时间。 由于每个处理器的系统内存视图不一致(不连续),此型号的机器被适当地命名为NUMA

为了扩展对 NUMA 机器的支持,内核将每个非统一内存分区(本地内存)视为node。 每个节点由描述符type pg_data_t标识,根据前面讨论的分区策略,该描述符引用该节点下的页面。 每个区域通过struct zone的实例表示。 UMA 机器将包含一个节点描述符,在该描述符下表示整个存储器,并且在 NUMA 机器上,枚举节点描述符列表,每个节点描述符表示一个连续的存储器节点。 下图说明了这些数据结构之间的关系:

接下来我们将介绍节点区域描述符数据结构定义。 请注意,我们不打算描述这些结构的每个元素,因为它们与内存管理的各个方面相关,这些方面超出了本章的范围。

节点描述符结构

节点描述符结构pg_data_t在内核标头mmzone.h中声明:

/* include/linux/mmzone.h */typedef struct pglist_data {
  struct zone node_zones[MAX_NR_ZONES];
 struct zonelist node_zonelists[MAX_ZONELISTS];
 int nr_zones;

#ifdef CONFIG_FLAT_NODE_MEM_MAP /* means !SPARSEMEM */
  struct page *node_mem_map;
#ifdef CONFIG_PAGE_EXTENSION
  struct page_ext *node_page_ext;
#endif
#endif

#ifndef CONFIG_NO_BOOTMEM
  struct bootmem_data *bdata;
#endif
#ifdef CONFIG_MEMORY_HOTPLUG
 spinlock_t node_size_lock;
#endif
 unsigned long node_start_pfn;
 unsigned long node_present_pages; /* total number of physical pages */
 unsigned long node_spanned_pages; 
 int node_id;
 wait_queue_head_t kswapd_wait;
 wait_queue_head_t pfmemalloc_wait;
 struct task_struct *kswapd; 
 int kswapd_order;
 enum zone_type kswapd_classzone_idx;

#ifdef CONFIG_COMPACTION
 int kcompactd_max_order;
 enum zone_type kcompactd_classzone_idx;
 wait_queue_head_t kcompactd_wait;
 struct task_struct *kcompactd;
#endif
#ifdef CONFIG_NUMA_BALANCING
 spinlock_t numabalancing_migrate_lock;
 unsigned long numabalancing_migrate_next_window;
 unsigned long numabalancing_migrate_nr_pages;
#endif
 unsigned long totalreserve_pages;

#ifdef CONFIG_NUMA
 unsigned long min_unmapped_pages;
 unsigned long min_slab_pages;
#endif /* CONFIG_NUMA */

 ZONE_PADDING(_pad1_)
 spinlock_t lru_lock;

#ifdef CONFIG_DEFERRED_STRUCT_PAGE_INIT
 unsigned long first_deferred_pfn;
#endif /* CONFIG_DEFERRED_STRUCT_PAGE_INIT */

#ifdef CONFIG_TRANSPARENT_HUGEPAGE
 spinlock_t split_queue_lock;
 struct list_head split_queue;
 unsigned long split_queue_len;
#endif
 unsigned int inactive_ratio;
 unsigned long flags;

 ZONE_PADDING(_pad2_)
 struct per_cpu_nodestat __percpu *per_cpu_nodestats;
 atomic_long_t vm_stat[NR_VM_NODE_STAT_ITEMS];
} pg_data_t;

根据所选的机器和内核配置的类型,各种元素都会编译到此结构中。 我们将介绍几个重要的元素:

| 菲尔德 (人名) | 描述 / 描写 / 形容 / 类别 | | node_zones | 保存此节点中页面的区域实例的数组。 | | node_zonelists | 指定节点中区域的首选分配顺序的数组。 | | nr_zones | 当前节点中的区域计数。 | | node_mem_map | 指向当前节点中的页面描述符列表的指针。 | | bdata | 指向引导内存描述符的指针(在后面部分讨论) | | node_start_pfn | 保存此节点中第一个物理页面的帧号;对于 UMA 系统,此值为。 | | node_present_pages | 节点中的总页数 | | node_spanned_pages | 物理页面范围的总大小,包括洞(如果有)。 | | node_id | 保存唯一节点标识符(节点从零开始编号) | | kswapd_wait | kswapd个内核线程的等待队列 | | kswapd | 指向kswapd内核线程的任务结构的指针 | | totalreserve_pages | 未用于用户空间分配的保留页数 |

区域描述符结构

mmzone.h报头还声明了struct zone,它充当区域描述符。 下面是结构定义的代码片段,注释很好。 接下来,我们将描述几个重要的领域:

struct zone {
 /* Read-mostly fields */

 /* zone watermarks, access with *_wmark_pages(zone) macros */
 unsigned long watermark[NR_WMARK];

 unsigned long nr_reserved_highatomic;

 /*
 * We don't know if the memory that we're going to allocate will be
 * freeable or/and it will be released eventually, so to avoid totally
 * wasting several GB of ram we must reserve some of the lower zone
 * memory (otherwise we risk to run OOM on the lower zones despite
 * there being tons of freeable ram on the higher zones). This array is
 * recalculated at runtime if the sysctl_lowmem_reserve_ratio sysctl
 * changes.
 */
 long lowmem_reserve[MAX_NR_ZONES];

#ifdef CONFIG_NUMA
 int node;
#endif
 struct pglist_data *zone_pgdat;
 struct per_cpu_pageset __percpu *pageset;

#ifndef CONFIG_SPARSEMEM
 /*
 * Flags for a pageblock_nr_pages block. See pageblock-flags.h.
 * In SPARSEMEM, this map is stored in struct mem_section
 */
 unsigned long *pageblock_flags;
#endif /* CONFIG_SPARSEMEM */

 /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
 unsigned long zone_start_pfn;

 /*
 * spanned_pages is the total pages spanned by the zone, including
 * holes, which is calculated as:
 * spanned_pages = zone_end_pfn - zone_start_pfn;
 *
 * present_pages is physical pages existing within the zone, which
 * is calculated as:
 * present_pages = spanned_pages - absent_pages(pages in holes);
 *
 * managed_pages is present pages managed by the buddy system, which
 * is calculated as (reserved_pages includes pages allocated by the
 * bootmem allocator):
 * managed_pages = present_pages - reserved_pages;
 *
 * So present_pages may be used by memory hotplug or memory power
 * management logic to figure out unmanaged pages by checking
 * (present_pages - managed_pages). And managed_pages should be used
 * by page allocator and vm scanner to calculate all kinds of watermarks
 * and thresholds.
 *
 * Locking rules:
 *
 * zone_start_pfn and spanned_pages are protected by span_seqlock.
 * It is a seqlock because it has to be read outside of zone->lock,
 * and it is done in the main allocator path. But, it is written
 * quite infrequently.
 *
 * The span_seq lock is declared along with zone->lock because it is
 * frequently read in proximity to zone->lock. It's good to
 * give them a chance of being in the same cacheline.
 *
 * Write access to present_pages at runtime should be protected by
 * mem_hotplug_begin/end(). Any reader who can't tolerant drift of
 * present_pages should get_online_mems() to get a stable value.
 *
 * Read access to managed_pages should be safe because it's unsigned
 * long. Write access to zone->managed_pages and totalram_pages are
 * protected by managed_page_count_lock at runtime. Idealy only
 * adjust_managed_page_count() should be used instead of directly
 * touching zone->managed_pages and totalram_pages.
 */
 unsigned long managed_pages;
 unsigned long spanned_pages;
 unsigned long present_pages;

 const char *name;// name of this zone

#ifdef CONFIG_MEMORY_ISOLATION
 /*
 * Number of isolated pageblock. It is used to solve incorrect
 * freepage counting problem due to racy retrieving migratetype
 * of pageblock. Protected by zone->lock.
 */
 unsigned long nr_isolate_pageblock;
#endif

#ifdef CONFIG_MEMORY_HOTPLUG
 /* see spanned/present_pages for more description */
 seqlock_t span_seqlock;
#endif

 int initialized;

 /* Write-intensive fields used from the page allocator */
 ZONE_PADDING(_pad1_)

 /* free areas of different sizes */
struct free_area free_area[MAX_ORDER];

 /* zone flags, see below */
 unsigned long flags;

 /* Primarily protects free_area */
 spinlock_t lock;

 /* Write-intensive fields used by compaction and vmstats. */
 ZONE_PADDING(_pad2_)

 /*
 * When free pages are below this point, additional steps are taken
 * when reading the number of free pages to avoid per-CPU counter
 * drift allowing watermarks to be breached
 */
 unsigned long percpu_drift_mark;

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
 /* pfn where compaction free scanner should start */
 unsigned long compact_cached_free_pfn;
 /* pfn where async and sync compaction migration scanner should start */
 unsigned long compact_cached_migrate_pfn[2];
#endif

#ifdef CONFIG_COMPACTION
 /*
 * On compaction failure, 1<<compact_defer_shift compactions
 * are skipped before trying again. The number attempted since
 * last failure is tracked with compact_considered.
 */
 unsigned int compact_considered;
 unsigned int compact_defer_shift;
 int compact_order_failed;
#endif

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
 /* Set to true when the PG_migrate_skip bits should be cleared */
 bool compact_blockskip_flush;
#endif

 bool contiguous;

 ZONE_PADDING(_pad3_)
 /* Zone statistics */
 atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;

以下是重要字段的汇总表,并对每个字段进行了简短说明:

| 菲尔德 (人名) | 描述 / 描写 / 形容 / 类别 | | watermark | 带WRMARK_MIN, WRMARK_LOWWRMARK_HIGH偏移量的无符号长整型数组。 这些偏移量中的值会影响kswapd内核线程执行的交换操作。 | | nr_reserved_highatomic | 保留的高位原子页的计数 | | lowmem_reserve | 数组,指定为关键分配保留的每个区域的页数 | | zone_pgdat | 指向此区域的节点描述符的指针。 | | pageset | 指向每个 CPU 的冷热页面列表的指针。 | | free_area | 类型为struct free_area的实例数组,每个实例抽象连续的空闲页面,供伙伴分配器使用。 更多关于伙伴分配器的信息将在后面的小节中介绍。 | | flags | 用于存储区域的当前状态的无符号长整型变量。 | | zone_start_pfn | 区域中第一页帧的索引 | | vm_stat | 的统计信息 |

内存分配器

了解了物理内存是如何组织的,并通过核心数据结构表示之后,我们现在将注意力转移到物理内存的管理上,以处理分配和释放请求。 内存分配请求可以由系统中的各种实体提出,例如用户模式进程、驱动程序和文件系统。 根据请求分配的实体和上下文的类型,返回的分配可能需要满足某些特征,例如物理上与页面对齐的连续大块或物理上连续的小块、硬件缓存对齐的内存或映射到虚拟连续地址空间的物理碎片块。

为了有效地管理物理内存,并根据所选择的优先级和模式来迎合内存,内核与一组内存分配器接合。 每个分配器都有一组不同的接口例程,这些例程由针对特定分配模式优化的精确设计的算法作为后盾。

页框分配器

也称为分区页帧分配器,它用作以倍数页大小进行物理连续分配的接口。 分配操作是通过查找空闲页面的适当区域来执行的。 每个区域中的物理页面由Buddy System管理,该系统充当页帧分配器的后端算法:

内核代码可以通过内核头linux/include/gfp.h:中提供的接口内联函数和宏在此算法上启动内存分配/释放操作

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

第一个参数gfp_mask用作指定要根据哪些分配执行属性的方法;我们将在接下来的小节中研究属性标志的详细信息。 第二个参数order用于指定分配的大小;分配的值被视为 2。 如果成功,则返回第一个页面结构的地址;如果失败,则返回 NULL。 对于单页分配,将提供一个备用宏,该宏再次依赖于alloc_pages()

#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0);

分配的页通过适当的页表条目(用于访问操作期间的分页地址转换)映射到连续的内核地址空间。 在页表映射之后生成的用于内核代码的地址称为线性地址。 通过另一个函数接口page_address(),调用程序代码可以检索分配的块的起始线性地址。

也可以通过一组包装器例程和到alloc_pages()的宏来启动分配,这些例程和宏略微扩展了功能,并返回所分配块的起始线性地址,而不是指向页面结构的指针。 下面的代码片段显示了包装函数和宏的列表:

/* allocates 2<sup class="calibre47">order</sup> pages and returns start linear address */ unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
struct page *page;
/*
* __get_free_pages() returns a 32-bit address, which cannot represent
* a highmem page
*/
VM_BUG_ON((gfp_mask & __GFP_HIGHMEM) != 0);

page = alloc_pages(gfp_mask, order);
if (!page)
return 0;
return (unsigned long) page_address(page);
}

/* Returns start linear address to zero initialized page */
unsigned long get_zeroed_page(gfp_t gfp_mask)
{
return __get_free_pages(gfp_mask | __GFP_ZERO, 0);
}
 /* Allocates a page */
#define __get_free_page(gfp_mask) \
__get_free_pages((gfp_mask), 0)

/* Allocate page/pages from DMA zone */
#define __get_dma_pages(gfp_mask, order) \
 __get_free_pages((gfp_mask) | GFP_DMA, (order))

以下是将内存释放回系统的接口。 我们需要调用与分配例程匹配的适当地址;传递错误的地址将导致损坏:

void __free_pages(struct page *page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
void free_page(addr);

搭伴制

页面分配器充当内存分配的接口(页面大小的倍数),而伙伴系统在后端操作以管理物理页面管理。 此算法管理每个区域的所有物理页。 它经过优化,通过最大限度地减少外部碎片来完成大型物理连续块(页)的分配。 让我们探索一下它的操作细节*。*

区域描述符结构包含一个由*struct free_area,*组成的数组,数组的大小通过内核宏MAX_ORDER定义,其默认值为11

  struct zone {
          ...
          ...
          struct free_area[MAX_ORDER];
          ...
          ...
     };

每个偏移量都包含free_area结构的一个实例。 所有空闲页被分成 11 个(MAX_ORDER)列表,每个列表包含 2页的块列表,顺序值在 0 到 11 的范围内(即,22的列表将包含 16KB 大小的块,23将包含 32KB 大小的块,依此类推)。 此策略可确保每个块自然对齐。 每个列表中的块的大小正好是较低列表中的块大小的两倍,从而导致更快的分配和释放操作。 它还为分配器提供了处理连续分配的能力,最大块大小为 8MB(211列表):

当提出特定大小的分配请求时,伙伴系统查找空闲块的适当列表,并返回其地址(如果可用)。 但是,如果它找不到空闲块,它会移动到下一个较大块的高位列表中签入,如果可用,它会将高位块拆分成称为伙伴的相等部分,为分配器返回 1,然后将第二个块排队到低位列表中。 当两个伙伴块在未来某个时间空闲时,它们将合并以创建一个更大的块。 算法可以通过对齐的地址识别伙伴块,这使得合并它们成为可能。

让我们考虑一个例子来更好地理解这一点,假设有一个分配 8k 块的请求(通过页面分配器例程)。 伙伴系统在free_pages数组的 8k 列表中查找空闲块(第一个偏移量包含 2 个1大小的块),并返回该块的起始线性地址(如果可用);但是,如果 8k 列表中没有空闲块,它将移动到下一个更高阶列表,该列表包含 16k 个块(free_pages数组的第二个偏移量),以找到空闲块。 让我们进一步假设该列表中也没有空闲块。 然后,它前进到下一个大小为 32k 的高阶列表(free_pages数组中的第三个偏移量)以查找空闲块;如果可用,它会将 32k 块拆分为两个相等的部分,每个部分为 16k(好友)。 第一个 16k 块被进一步分割成两个 8k 的部分(个伙伴),其中一个分配给调用者,另一个放入 8k 列表中。 16k 的第二个块被放入 16k 空闲列表中,当较低阶(8k)的伙伴在未来某个时间变得空闲时,它们被合并以形成较高阶的 16k 块。 当两个 16k 伙伴都空闲时,它们再次合并到 32k 块,该块被放回到空闲列表中。

当无法处理来自期望的区域的分配请求时,伙伴系统使用后备机制来查找其他区域和节点:

伙伴系统有着悠久的历史,通过适当的优化在各种nix 操作系统上广泛实施。 如前所述,它有助于更快地分配和释放内存,并且在一定程度上还可以最大程度地减少外部碎片。 随着提供急需的性能优势的巨型页面的出现,进一步努力反碎片变得更加重要。 为了实现这一点,Linux 内核的伙伴系统实现通过页面迁移配备了防碎片能力。*

页面迁移将虚拟页面的*数据从一个物理存储区域移动到另一个物理存储区域的处理。 此机制有助于创建具有连续页面的较大块。 为了实现这一点,将页面分类为以下类型:

1.不可移动页面:固定并为特定分配保留的物理页面被认为是不可移动的。 为核心内核固定的页面就属于这一类。 这些页面是不可回收的。

2.可回收页:映射到动态分配的物理页可以被逐出到后备库,而那些可以重新生成的页被认为是可回收页。 用于文件缓存的页面、匿名页面映射以及由内核的板片缓存保存的页面都属于这一类。 回收操作有两种模式:定期回收和直接回收,前者通过名为*kswapd的 k 线程实现。* 当系统运行时内存过少时,内核进入直接回收。

**3.可移动页面:**可通过页面迁移机制将移动到不同区域的物理页面。 映射到用户模式进程的虚拟地址空间的页面被认为是可移动的,因为 VM 子系统需要做的全部工作就是复制数据和更改相关的页面表项。 考虑到来自用户模式进程的所有访问操作都经过页表转换,这是可行的。

伙伴系统基于可移动性将页面分组为独立列表,并使用它们进行适当的分配。 这是通过基于页面移动性将struct free_area中的每个 2n列表组织为一组自治列表来实现的。 每个free_area实例包含一个大小为MIGRATE_TYPES的列表数组。 每个偏移量保存各自页组的list_head

 struct free_area {
          struct list_head free_list[MIGRATE_TYPES];
          unsigned long nr_free;
    };

nr_free是一个计数器,它保存此free_area的空闲页面总数(将所有迁移列表放在一起)。 下图描述了每种迁移类型的空闲列表:

以下枚举定义了页面迁移类型:

enum {
 MIGRATE_UNMOVABLE,
 MIGRATE_MOVABLE,
 MIGRATE_RECLAIMABLE,
 MIGRATE_PCPTYPES, /* the number of types on the pcp lists */
 MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
 MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
 MIGRATE_ISOLATE, /* can't allocate from here */
#endif
 MIGRATE_TYPES
};  

我们已经讨论了关键迁移类型MIGRATE_MOVABLEMIGRATE_UNMOVABLEMIGRATE_RECLAIMABLE类型。 MIGRATE_PCPTYPES是为提高系统性能而引入的一种特殊类型;每个区域在每个 CPU 页面缓存中维护一个缓存热页面列表。 这些页面用于服务本地 CPU 提出的分配请求。 zone描述符结构pageset元素指向每个 CPU 缓存中的页面:

/* include/linux/mmzone.h */

struct per_cpu_pages {
 int count; /* number of pages in the list */
 int high; /* high watermark, emptying needed */
 int batch; /* chunk size for buddy add/remove */

 /* Lists of pages, one per migrate type stored on the pcp-lists */
 struct list_head lists[MIGRATE_PCPTYPES];
};

struct per_cpu_pageset {
 struct per_cpu_pages pcp;
#ifdef CONFIG_NUMA
 s8 expire;
#endif
#ifdef CONFIG_SMP
 s8 stat_threshold;
 s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS];
#endif
};

struct zone {
 ...
 ...
 struct per_cpu_pageset __percpu *pageset;
 ...
 ...
};

struct per_cpu_pageset是表示不可移动可回收可移动页面列表的抽象。 MIGRATE_PCPTYPES是按页移动性排序的每个 CPU 页列表的计数。 MIGRATE_CMA是连续内存分配器的页面列表,我们将在后续章节中讨论:

伙伴系统被实现为在备选列表上后退,以在所需移动性的页面不可用时处理分配请求。 以下数组定义了各种迁移类型的回退顺序;我们将不再详细说明,因为这是不言而喻的:

static int fallbacks[MIGRATE_TYPES][4] = {
 [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_TYPES },
 [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE, MIGRATE_TYPES },
 [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_TYPES },
#ifdef CONFIG_CMA
 [MIGRATE_CMA] = { MIGRATE_TYPES }, /* Never used */
#endif
#ifdef CONFIG_MEMORY_ISOLATION
 [MIGRATE_ISOLATE] = { MIGRATE_TYPES }, /* Never used */
#endif
};

GFP 掩模

页面分配器和其他分配器例程(我们将在以下各节中讨论)需要gfp_mask标志作为参数,其类型为gfp_t

typedef unsigned __bitwise__ gfp_t;

GFP 标志用于为分配器函数提供两个重要属性:第一个是分配的模式,它控制分配器函数*、的行为;第二个是分配的源*,它指示可以从中获取内存的的列表*。* 内核标题gfp.h定义了各种标记常量,这些标记常量被分类为不同的组,称为区域修改符、移动性和****放置标记、水印修改符、回收修改符、和动作修改符。

分区修改器

以下是用于指定内存来源的区域的修饰符的汇总列表。 回想一下我们在前一节中关于区域的讨论;对于每个区域,都定义了一个gfp标志:

#define __GFP_DMA ((__force gfp_t)___GFP_DMA)
#define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM)
#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32)
#define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* ZONE_MOVABLE allowed */

页面移动和放置

下面的代码片断定义了页面移动和放置标志:

#define __GFP_RECLAIMABLE ((__force gfp_t)___GFP_RECLAIMABLE)
#define __GFP_WRITE ((__force gfp_t)___GFP_WRITE)
#define __GFP_HARDWALL ((__force gfp_t)___GFP_HARDWALL)
#define __GFP_THISNODE ((__force gfp_t)___GFP_THISNODE)
#define __GFP_ACCOUNT ((__force gfp_t)___GFP_ACCOUNT)

以下是页面移动和放置标志的列表:

  • __GFP_RECLAIMABLE:大多数内核子系统设计为使用内存缓存来缓存经常需要的资源,如数据结构、内存块、持久文件数据等。 内存管理器维护这样的高速缓存,并允许它们按需动态扩展。 但是,不能允许这样的缓存无限扩展,否则它们最终会消耗所有内存。 内存管理器通过收缩器接口处理此问题,内存管理器可以通过该机制收缩缓存,并在需要时回收页面。 在分配页(用于高速缓存)时启用该标志是对缩缩器的指示,表明该页是可回收的。 该标志由片分配器使用,这将在后面的小节中讨论。
  • __GFP_WRITE:当使用此标志时,它向内核指示调用者打算弄脏页面。 存储器管理器根据公平区域分配策略分配适当的页面,该策略在节点的本地区域上循环分配这样的页面,以避免所有脏页都在一个区域中。
  • __GFP_HARDWALL:此标志确保在调用方绑定到的同一个或多个节点上执行分配;换句话说,它强制执行 CPUSET 内存分配策略。
  • __GFP_THISNODE:此标志强制从请求的节点满足分配,而不执行后备或放置策略。
  • __GFP_ACCOUNT:此标志使分配计入 kmem 控制组。

水印修饰符

以下代码片断定义了水印修饰符:

#define __GFP_ATOMIC ((__force gfp_t)___GFP_ATOMIC)
#define __GFP_HIGH ((__force gfp_t)___GFP_HIGH)
#define __GFP_MEMALLOC ((__force gfp_t)___GFP_MEMALLOC)
#define __GFP_NOMEMALLOC ((__force gfp_t)___GFP_NOMEMALLOC)

以下是提供对紧急保留内存池的控制的水印修改器列表:

  • __GFP_ATOMIC:此标志指示分配优先级高,调用方上下文无法进入等待状态。
  • __GFP_HIGH:此标志表示调用者优先级高,系统需要批准分配请求才能取得进展。 设置此标志将导致分配器访问应急池。
  • __GFP_MEMALLOC:此标志允许访问所有内存。 这应该仅在调用方保证分配将允许很快释放更多内存时使用,例如,进程退出或交换。
  • __GFP_NOMEMALLOC:该标志用于禁止访问所有预留的应急池。

页面回收修饰符

随着系统负载的增加,区域中的空闲内存量可能会低于低水位线,从而导致内存紧缩,这将严重影响系统的整体性能*。* 为了处理这种情况,存储器管理器配备了页回收算法,它们被实现来识别和回收页。 内核内存分配器例程,在使用称为页面回收修饰符的适当 GFP 常量调用时,使用回收算法:

#define __GFP_IO ((__force gfp_t)___GFP_IO)
#define __GFP_FS ((__force gfp_t)___GFP_FS)
#define __GFP_DIRECT_RECLAIM ((__force gfp_t)___GFP_DIRECT_RECLAIM) /* Caller can reclaim */
#define __GFP_KSWAPD_RECLAIM ((__force gfp_t)___GFP_KSWAPD_RECLAIM) /* kswapd can wake */
#define __GFP_RECLAIM ((__force gfp_t)(___GFP_DIRECT_RECLAIM|___GFP_KSWAPD_RECLAIM))
#define __GFP_REPEAT ((__force gfp_t)___GFP_REPEAT)
#define __GFP_NOFAIL ((__force gfp_t)___GFP_NOFAIL)
#define __GFP_NORETRY ((__force gfp_t)___GFP_NORETRY)

以下是可以作为参数传递给分配例程的回收修饰符的列表;每个标志都启用对特定内存区域的回收操作:

  • __GFP_IO:此标志指示分配器可以启动物理 I/O(交换)来回收内存。
  • __GFP_FS:此标志表示分配器可以向下调用低级别 FS 进行回收。
  • __GFP_DIRECT_RECLAIM:此标志表示调用者愿意进入直接回收。 这可能会导致调用方阻塞。
  • __GFP_KSWAPD_RECLAIM:此标志指示当达到低水位线时,分配器可以唤醒kswapd内核线程以启动回收。
  • __GFP_RECLAIM:该标志用于启用直接回收和kswapd回收。
  • __GFP_REPEAT:此标志指示尝试分配内存,但分配尝试可能会失败。
  • __GFP_NOFAIL:此标志强制虚拟内存管理器重试,直到分配请求。 成功了。 这可能会导致 VM 触发 OOM 杀手回收内存。
  • __GFP_NORETRY:此标志将使分配器在无法处理请求时返回相应的失败状态。

动作修饰符

下面的代码片断定义了操作修饰符:

#define __GFP_COLD ((__force gfp_t)___GFP_COLD)
#define __GFP_NOWARN ((__force gfp_t)___GFP_NOWARN)
#define __GFP_COMP ((__force gfp_t)___GFP_COMP)
#define __GFP_ZERO ((__force gfp_t)___GFP_ZERO)
#define __GFP_NOTRACK ((__force gfp_t)___GFP_NOTRACK)
#define __GFP_NOTRACK_FALSE_POSITIVE (__GFP_NOTRACK)
#define __GFP_OTHER_NODE ((__force gfp_t)___GFP_OTHER_NODE)

以下是操作修改器标志的列表;这些标志指定分配器例程在处理请求时要考虑的其他属性:

  • __GFP_COLD:为了实现快速访问,将每个区域中的几个页面缓存到每个 CPU 缓存中;缓存中保存的页面称为HOT,未缓存的页面称为COLD。 此标志指示分配器应通过缓存冷页服务内存请求。
  • __GFP_NOWARN:此标志使分配器在静默模式下运行,从而导致不报告警告和错误条件。
  • __GFP_COMP:该标志用于分配具有适当元数据的复合页。 复合页面是由两个或多个物理上连续的页面组成的组,它们被视为单个大页面。 元数据使复合页面有别于其他物理上连续的页面。 复合页的第一个物理页称为头页,其页描述符中设置了PG_head标志,其余页称为尾页
  • __GFP_ZERO:此标志使分配器返回填充为零的页。
  • __GFP_NOTRACK:kmemcheck 是内核内调试器之一,用于检测和警告未初始化的内存访问。 尽管如此,这样的检查会导致存储器访问操作延迟。 当性能是一个标准时,调用方可能希望分配 kmemcheck 不跟踪的内存。 此标志使分配器返回此类内存。
  • __GFP_NOTRACK_FALSE_POSITIVE:该标志是**__GFP_NOTRACK**的别名。
  • __GFP_OTHER_NODE:该标志用于透明巨型页面(THP)的分配。

类型标志

由于修饰符标志的类别如此之多(每种类型都针对不同的属性),程序员在为相应的分配选择标志时会格外小心。 为了使这一过程更容易、更快,引入了类型标志,使程序员能够快速做出分配选择。 类型标志派生自特定分配用例的各种修改器常量(前面列出)的组合。 但是,如果需要,程序员可以进一步自定义类型标志:

#define GFP_ATOMIC (__GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM)
#define GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS)
#define GFP_KERNEL_ACCOUNT (GFP_KERNEL | __GFP_ACCOUNT)
#define GFP_NOWAIT (__GFP_KSWAPD_RECLAIM)
#define GFP_NOIO (__GFP_RECLAIM)
#define GFP_NOFS (__GFP_RECLAIM | __GFP_IO)
#define GFP_TEMPORARY (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_RECLAIMABLE)
#define GFP_USER (__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL)
#define GFP_DMA __GFP_DMA
#define GFP_DMA32 __GFP_DMA32
#define GFP_HIGHUSER (GFP_USER | __GFP_HIGHMEM)
#define GFP_HIGHUSER_MOVABLE (GFP_HIGHUSER | __GFP_MOVABLE)
#define GFP_TRANSHUGE_LIGHT ((GFP_HIGHUSER_MOVABLE | __GFP_COMP | __GFP_NOMEMALLOC | \ __GFP_NOWARN) & ~__GFP_RECLAIM)
#define GFP_TRANSHUGE (GFP_TRANSHUGE_LIGHT | __GFP_DIRECT_RECLAIM)

以下是类型标志列表:

  • GFP_ATOMIC:此标志是为不会失败的非阻塞分配指定的。 此标志将从应急储备中进行分配。 这通常在从原子上下文调用分配器时使用。
  • GFP_KERNEL:在分配内核使用的内存时使用此标志。 这些请求是从普通区域处理的。 此标志可能会使分配器进入直接回收。
  • GFP_KERNEL_ACCOUNT:与GFP_KERNEL相同,但增加了由 kmem 控制组跟踪分配。
  • GFP_NOWAIT:此标志用于非阻塞的内核分配。
  • GFP_NOIO:此标志允许分配器在不需要物理 I/O(交换)的干净页面上开始直接回收。
  • GFP_NOFS:此标志允许分配器开始直接回收,但阻止调用文件系统接口。
  • GFP_TEMPORARY:在为内核缓存分配页面时使用此标志,这些页面可通过适当的缩减器接口回收。 该标志设置我们前面讨论的__GFP_RECLAIMABLE标志。
  • GFP_USER:该标志用于用户空间分配。 分配的内存映射到用户进程,也可以由内核服务或硬件访问,以便将 DMA 从设备传输到缓冲区,反之亦然。
  • GFP_DMA:此标志导致从最低的区域(称为ZONE_DMA)进行分配。 为了向后兼容,仍支持此标志。
  • GFP_DMA32:此标志导致从ZONE_DMA32处理分配,ZONE_DMA32包含<4G 内存中的页面。
  • GFP_HIGHUSER:此标志用于从**ZONE_HIGHMEM**分配用户空间(仅与 32 位平台相关)。
  • GFP_HIGHUSER_MOVABLE:此标志类似于GFP_HIGHUSER,但增加了从可移动页面执行分配,从而实现页面迁移和回收。
  • GFP_TRANSHUGE_LIGHT:这会导致透明巨大分配(THP)的分配,这是复合分配。 此类型标志设置__GFP_COMP,我们在前面讨论过。

板坯分配器

正如前面几节所讨论的,页面分配器(与伙伴系统协作)有效地处理页面大小倍数的内存分配请求。 然而,内核代码内部使用的大多数分配请求都是针对较小的块(通常小于一页);使用页面分配器进行此类分配会导致*内部碎片,*导致内存浪费。 片分配器正是为解决这一问题而实现的;它构建在伙伴系统之上,用于分配较小的内存块,以保存内核服务使用的结构对象或数据。

片分配器的设计基于对象**缓存的思想。 对象缓存的概念非常简单:它包括保留一组空闲页帧,将它们划分和组织成称为片缓存的独立空闲列表(每个列表包含几个空闲页面),并使用每个列表分配固定大小的对象池或内存块,称为单元。 这样,每个列表都被分配了唯一的单元大小,并且将包含该大小的对象池或内存块。 当对给定大小的存储器块的分配请求到达时,分配器算法选择其单元大小最适合于所请求的大小的适当的片高速缓存,并返回空闲块的地址。

然而,在较低的级别上,在片缓存的初始化和管理方面涉及到相当复杂的问题。 该算法需要考虑目标跟踪、动态扩展、通过缩放器接口进行安全回收等各种问题。 解决所有这些问题并在增强的性能和最佳的内存占用之间实现适当的平衡是一个相当大的挑战。 我们将在后续小节中更多地探讨这些挑战,但现在我们将继续讨论分配器函数接口。

Kmalloc 缓存

片分配器维护一组通用片高速缓存,用于以 8 的倍数缓存单元大小的内存块。它为每个单元大小维护两组片高速缓存,一组用于维护从ZONE_NORMAL页分配的内存块池,另一组用于维护从ZONE_DMA页分配的内存块池。 这些缓存是全局的,由所有内核代码共享。 用户可以通过特殊文件/proc/slabinfo跟踪这些缓存的状态。 内核服务可以通过kmalloc系列例程从这些缓存分配和释放内存块。 它们称为kmalloc缓存:

#cat /proc/slabinfo 
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>dma-kmalloc-8192 0 0 8192 4 8 : tunables 0 0 0 : slabdata 0 0 0
dma-kmalloc-4096 0 0 4096 8 8 : tunables 0 0 0 : slabdata 0 0 0
dma-kmalloc-2048 0 0 2048 16 8 : tunables 0 0 0 : slabdata 0 0 0
dma-kmalloc-1024 0 0 1024 16 4 : tunables 0 0 0 : slabdata 0 0 0
dma-kmalloc-512 0 0 512 16 2 : tunables 0 0 0 : slabdata 0 0 0
dma-kmalloc-256 0 0 256 16 1 : tunables 0 0 0 : slabdata 0 0 0
dma-kmalloc-128 0 0 128 32 1 : tunables 0 0 0 : slabdata 0 0 0
dma-kmalloc-64 0 0 64 64 1 : tunables 0 0 0 : slabdata 0 0 0
dma-kmalloc-32 0 0 32 128 1 : tunables 0 0 0 : slabdata 0 0 0
dma-kmalloc-16 0 0 16 256 1 : tunables 0 0 0 : slabdata 0 0 0
dma-kmalloc-8 0 0 8 512 1 : tunables 0 0 0 : slabdata 0 0 0
dma-kmalloc-192 0 0 192 21 1 : tunables 0 0 0 : slabdata 0 0 0
dma-kmalloc-96 0 0 96 42 1 : tunables 0 0 0 : slabdata 0 0 0
kmalloc-8192 156 156 8192 4 8 : tunables 0 0 0 : slabdata 39 39 0
kmalloc-4096 325 352 4096 8 8 : tunables 0 0 0 : slabdata 44 44 0
kmalloc-2048 1105 1184 2048 16 8 : tunables 0 0 0 : slabdata 74 74 0
kmalloc-1024 2374 2448 1024 16 4 : tunables 0 0 0 : slabdata 153 153 0
kmalloc-512 1445 1520 512 16 2 : tunables 0 0 0 : slabdata 95 95 0
kmalloc-256 9988 10400 256 16 1 : tunables 0 0 0 : slabdata 650 650 0
kmalloc-192 3561 4053 192 21 1 : tunables 0 0 0 : slabdata 193 193 0
kmalloc-128 3588 5728 128 32 1 : tunables 0 0 0 : slabdata 179 179 0
kmalloc-96 3402 3402 96 42 1 : tunables 0 0 0 : slabdata 81 81 0
kmalloc-64 42672 45184 64 64 1 : tunables 0 0 0 : slabdata 706 706 0
kmalloc-32 15095 16000 32 128 1 : tunables 0 0 0 : slabdata 125 125 0
kmalloc-16 6400 6400 16 256 1 : tunables 0 0 0 : slabdata 25 25 0
kmalloc-8 6144 6144 8 512 1 : tunables 0 0 0 : slabdata 12 12 0

kmalloc-96kmalloc-192是用于维护与 1 级硬件高速缓存对齐的内存块的高速缓存。 对于超过 8k(大块)的分配,片分配器依赖于伙伴系统。 以下是 kmalloc 系列分配器例程;所有这些都需要适当的 GFP 标志:

/**
 * kmalloc - allocate memory. 
 * @size: bytes of memory required.
 * @flags: the type of memory to allocate.
 */
 void *kmalloc(size_t size, gfp_t flags) /**
 * kzalloc - allocate memory. The memory is set to zero.
 * @size: bytes of memory required.
 * @flags: the type of memory to allocate.
 */
 inline void *kzalloc(size_t size, gfp_t flags) /**
 * kmalloc_array - allocate memory for an array.
 * @n: number of elements.
 * @size: element size.
 * @flags: the type of memory to allocate (see kmalloc).
 */ 
 inline void *kmalloc_array(size_t n, size_t size, gfp_t flags) /**
 * kcalloc - allocate memory for an array. The memory is set to zero.
 * @n: number of elements.
 * @size: element size.
 * @flags: the type of memory to allocate (see kmalloc).
 */ inline void *kcalloc(size_t n, size_t size, gfp_t flags) /**
 * krealloc - reallocate memory. The contents will remain unchanged.
 * @p: object to reallocate memory for.
 * @new_size: bytes of memory are required.
 * @flags: the type of memory to allocate.
 *
 * The contents of the object pointed to are preserved up to the 
 * lesser of the new and old sizes. If @p is %NULL, krealloc()
 * behaves exactly like kmalloc(). If @new_size is 0 and @p is not a
 * %NULL pointer, the object pointed to is freed
 */
 void *krealloc(const void *p, size_t new_size, gfp_t flags) /**
 * kmalloc_node - allocate memory from a particular memory node.
 * @size: bytes of memory are required.
 * @flags: the type of memory to allocate.
 * @node: memory node from which to allocate
 */ void *kmalloc_node(size_t size, gfp_t flags, int node) /**
 * kzalloc_node - allocate zeroed memory from a particular memory node.
 * @size: how many bytes of memory are required.
 * @flags: the type of memory to allocate (see kmalloc).
 * @node: memory node from which to allocate
 */ void *kzalloc_node(size_t size, gfp_t flags, int node)

以下例程将分配的块返回到空闲池。 调用方需要确保作为参数传递的地址属于有效的已分配块:

/**
 * kfree - free previously allocated memory
 * @objp: pointer returned by kmalloc.
 *
 * If @objp is NULL, no operation is performed.
 *
 * Don't free memory not originally allocated by kmalloc()
 * or you will run into trouble.
 */
void kfree(const void *objp) /**
 * kzfree - like kfree but zero memory
 * @p: object to free memory of
 *
 * The memory of the object @p points to is zeroed before freed.
 * If @p is %NULL, kzfree() does nothing.
 *
 * Note: this function zeroes the whole allocated buffer which can be a good
 * deal bigger than the requested buffer size passed to kmalloc(). So be
 * careful when using this function in performance sensitive code.
 */ void kzfree(const void *p)

对象缓存

片分配器提供用于设置片缓存的函数接口,片缓存可以由内核服务或子系统拥有。 这样的缓存被认为是私有的,因为它们是内核服务(或内核子系统)(如设备驱动程序、文件系统、进程调度程序等)的本地缓存。 大多数内核子系统都使用此工具来设置对象缓存,并将间歇性需要的数据结构放入池中。 到目前为止,我们遇到的大多数数据结构(从第 1 章理解进程、地址空间和线程开始),包括进程描述符、信号描述符、页面描述符等等,都是在这样的对象池中维护的。 伪文件/proc/slabinfo显示对象缓存的状态:

# cat /proc/slabinfo 
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
sigqueue 100 100 160 25 1 : tunables 0 0 0 : slabdata 4 4 0
bdev_cache 76 76 832 19 4 : tunables 0 0 0 : slabdata 4 4 0
kernfs_node_cache 28594 28594 120 34 1 : tunables 0 0 0 : slabdata 841 841 0
mnt_cache 489 588 384 21 2 : tunables 0 0 0 : slabdata 28 28 0
inode_cache 15932 15932 568 28 4 : tunables 0 0 0 : slabdata 569 569 0
dentry 89541 89817 192 21 1 : tunables 0 0 0 : slabdata 4277 4277 0
iint_cache 0 0 72 56 1 : tunables 0 0 0 : slabdata 0 0 0
buffer_head 53079 53430 104 39 1 : tunables 0 0 0 : slabdata 1370 1370 0
vm_area_struct 41287 42400 200 20 1 : tunables 0 0 0 : slabdata 2120 2120 0
files_cache 207 207 704 23 4 : tunables 0 0 0 : slabdata 9 9 0
signal_cache 420 420 1088 30 8 : tunables 0 0 0 : slabdata 14 14 0
sighand_cache 289 315 2112 15 8 : tunables 0 0 0 : slabdata 21 21 0
task_struct 750 801 3584 9 8 : tunables 0 0 0 : slabdata 89 89 0

*kmem_cache_create()*例程根据传递的参数设置新的缓存。 如果成功,它会将地址返回到类型为*kmem_cache*的高速缓存描述符结构:

/*
 * kmem_cache_create - Create a cache.
 * @name: A string which is used in /proc/slabinfo to identify this cache.
 * @size: The size of objects to be created in this cache.
 * @align: The required alignment for the objects.
 * @flags: SLAB flags
 * @ctor: A constructor for the objects.
 *
 * Returns a ptr to the cache on success, NULL on failure.
 * Cannot be called within a interrupt, but can be interrupted.
 * The @ctor is run when new pages are allocated by the cache.
 *
 */
struct kmem_cache * kmem_cache_create(const char *name, size_t size, size_t align,
                                      unsigned long flags, void (*ctor)(void *))

通过(从伙伴系统)分配空闲页帧来创建高速缓存,并且填充指定的大小的数据对象(第二参数)。 虽然每个缓存在创建期间都是通过托管固定数量的数据对象开始的,但它们可以在需要容纳更多数量的数据对象时动态增长。 数据结构可能很复杂(我们已经遇到过一些),并且可以包含各种元素,如列表头、子对象、数组、原子计数器、位域等。 设置每个对象可能需要将其所有字段初始化为默认状态;这可以通过分配给*ctor函数指针(最后一个参数)的初始化器例程来实现。 无论是在缓存创建期间还是在缓存增长以添加更多空闲对象时,都会为分配的每个新对象调用初始化器。 但是,对于简单对象,可以在没有初始化式的情况下创建缓存

以下是显示kmem_cache_create()用法的示例代码片段:

/* net/core/skbuff.c */

struct kmem_cache *skbuff_head_cache;
skbuff_head_cache = kmem_cache_create("skbuff_head_cache",sizeof(struct sk_buff), 0, 
                                       SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL);                                                                                                   

标志用于启用调试检查,并通过将对象与硬件缓存对齐来增强对缓存的访问操作的性能。 支持以下标志常量:

 SLAB_CONSISTENCY_CHECKS /* DEBUG: Perform (expensive) checks o alloc/free */
 SLAB_RED_ZONE /* DEBUG: Red zone objs in a cache */
 SLAB_POISON /* DEBUG: Poison objects */
 SLAB_HWCACHE_ALIGN  /* Align objs on cache lines */
 SLAB_CACHE_DMA  /* Use GFP_DMA memory */
 SLAB_STORE_USER  /* DEBUG: Store the last owner for bug hunting */
 SLAB_PANIC  /* Panic if kmem_cache_create() fails */

随后,可以通过相关函数分配和释放个对象。 在释放时,对象被放回高速缓存的空闲列表中,使它们可供重用;这可能会提高性能,特别是当对象是热高速缓存时:

/**
 * kmem_cache_alloc - Allocate an object
 * @cachep: The cache to allocate from.
 * @flags: GFP mask.
 *
 * Allocate an object from this cache. The flags are only relevant
 * if the cache has no available objects.
 */
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);

/**
 * kmem_cache_alloc_node - Allocate an object on the specified node
 * @cachep: The cache to allocate from.
 * @flags: GFP mask.
 * @nodeid: node number of the target node.
 *
 * Identical to kmem_cache_alloc but it will allocate memory on the given
 * node, which can improve the performance for cpu bound structures.
 *
 * Fallback to other node is possible if __GFP_THISNODE is not set.
 */
void *kmem_cache_alloc_node(struct kmem_cache *cachep, gfp_t flags, int nodeid); /**
 * kmem_cache_free - Deallocate an object
 * @cachep: The cache the allocation was from.
 * @objp: The previously allocated object.
 *
 * Free an object which was previously allocated from this
 * cache.
 */ void kmem_cache_free(struct kmem_cache *cachep, void *objp);

当所有托管数据对象都是空闲的(未使用)*,*时,可以通过调用kmem_cache_destroy().来销毁 kmem 缓存

缓存管理

所有片缓存都由片核心在内部管理,这是一种低级算法。 它定义了描述每个缓存列表的物理布局的各种控制结构,并实现了由接口例程调用的核心缓存管理操作。 基于 Bonwick 的一篇论文,片分配器最初是在 Solaris2.4 内核中实现的,并被大多数其他*nix 内核使用。

传统上,Linux 用于内存适中的单处理器台式机和服务器系统,内核采用 Bonwick 的经典模型,并进行了适当的性能改进。 多年来,由于移植和使用 Linux 内核的平台不同,优先级不同,传统的 SLAB 核心算法实现效率很低,无法满足所有需求。 虽然内存受限的嵌入式平台无法承受较高的分配器占用空间(用于管理元数据和分配器操作密度的空间),但具有巨大内存的 SMP 系统需要一致的性能、可伸缩性和更好的机制来生成有关分配的跟踪和调试信息。

为了迎合这些不同的需求,内核的当前版本提供了三种截然不同的板条算法实现:slob,一个经典的 K&R 型列表分配器,专为内存不足的系统设计,在最初几年(1991-1999)是 Linux 的默认对象分配器;slub,一个自 1999 年以来一直在 Linux 中出现的经典 Solaris 风格的板条分配器;以及slub,经过改进。 大多数架构的默认内核配置启用slub作为默认板分配器;这可以在内核构建期间通过内核配置选项进行更改。

CONFIG_SLAB: The regular slab allocator that is established and known to work well in all environments. It organizes cache hot objects in per-CPU and per node queues.

CONFIG_SLUB: SLUB is a slab allocator that minimizes cache line usage instead of managing queues of cached objects (SLAB approach). per-CPU caching is realized using slabs of objects instead of queues of objects. SLUB can use memory efficiently and has enhanced diagnostics. SLUB is the default choice for a slab allocator.

CONFIG_SLOB: SLOB replaces the stock allocator with a drastically simpler allocator. SLOB is generally more space efficient but does not perform as well on large systems.

无论选择哪种类型的分配器,编程接口都保持不变。 事实上,在较低级别,所有三个分配器都共享一些公共代码库:

现在,我们将研究高速缓存的物理布局及其控制结构。

缓存布局-通用

每个高速缓存由高速缓存描述符结构kmem_cache表示;该结构包含高速缓存的所有关键元数据。 它包括一个板描述符列表,每个板描述符托管一个页面或一组页框*。* 片下的页面包含对象或内存块,它们是高速缓存的分配单元片描述符指向页面中包含的对象列表,并跟踪它们的状态。 根据它所承载的对象的状态,板可能处于三种可能的状态之一--已满、部分或空。 如果 sLab的所有对象都正在使用,并且没有个空闲的个对象可供分配,则该 sLab被视为已满具有至少一个自由对象的板材被认为处于部分状态,而具有所有对象处于自由状态的板材被认为是空的*。*

*

这种安排实现了对象的快速分配,因为分配器例程可以在Partial板中查找空闲对象,如果需要,还可以移动到空的板。 它还有助于使用新的页帧更轻松地扩展高速缓存以容纳更多对象(在需要时),并有助于安全快速地回收(可以回收状态的存储片)。

辅助数据结构

了解了一般级别的缓存和描述符的布局之后,让我们进一步查看slub分配器使用的特定数据结构,并探索空闲列表的管理。 Slub在内核标头/include/linux/slub-def.h中定义其高速缓存描述符struct kmem_cache的版本:

struct kmem_cache {
 struct kmem_cache_cpu __percpu *cpu_slab;
 /* Used for retriving partial slabs etc */
 unsigned long flags;
 unsigned long min_partial;
 int size; /* The size of an object including meta data */
 int object_size; /* The size of an object without meta data */
 int offset; /* Free pointer offset. */
 int cpu_partial; /* Number of per cpu partial objects to keep around */
 struct kmem_cache_order_objects oo;

 /* Allocation and freeing of slabs */
 struct kmem_cache_order_objects max;
 struct kmem_cache_order_objects min;
 gfp_t allocflags; /* gfp flags to use on each alloc */
 int refcount; /* Refcount for slab cache destroy */
 void (*ctor)(void *);
 int inuse; /* Offset to metadata */
 int align; /* Alignment */
 int reserved; /* Reserved bytes at the end of slabs */
 const char *name; /* Name (only for display!) */
 struct list_head list; /* List of slab caches */
 int red_left_pad; /* Left redzone padding size */
 ...
 ...
 ...
 struct kmem_cache_node *node[MAX_NUMNODES];
};

list元素引用一组片缓存。 当分配新的片时,它被存储在高速缓存描述符中的列表中,并且被认为是空的,,因为它的所有对象都是空闲的并且可用。 在分配对象时,板材变为部分状态。 部分片是分配器需要跟踪的唯一类型的片,并且在kmem_cache结构内的列表中连接。 SLUB分配器对跟踪对象已全部分配的块,或对象为空闲块没有兴趣。 SLUB通过类型为struct kmem_cache_node[MAX_NUMNODES]的指针数组跟踪每个节点的部分切片,该指针数组封装了部分切片的列表:

struct kmem_cache_node {
 spinlock_t list_lock;
 ...
 ...
#ifdef CONFIG_SLUB
 unsigned long nr_partial;
 struct list_head partial;
#ifdef CONFIG_SLUB_DEBUG
 atomic_long_t nr_slabs;
 atomic_long_t total_objects;
 struct list_head full;
#endif
#endif 
};

板中的所有空闲对象形成一个链表;当分配请求到达时,第一个空闲对象从列表中删除,其地址返回给调用方。 通过链接列表跟踪自由对象需要大量的元数据;传统的板片分配器在板头中维护板片的所有页面的元数据(导致数据对齐问题),而SLUB通过在页面描述符结构中塞入更多字段来维护板片中页面的每页元数据,从而从板头中删除元数据。 页面描述符中的 SLUB元数据元素仅当相应的页面是板条的一部分时才有效。 参与片分配的页面设置了PG_slab标志。

以下是与 SLUB 相关的页面描述符字段:

struct page {
      ...
      ...
     union {
      pgoff_t index; /* Our offset within mapping. */
      void *freelist; /* sl[aou]b first free object */
   };
     ...
     ...
   struct {
          union {
                  ...
                   struct { /* SLUB */
                          unsigned inuse:16;
                          unsigned objects:15;
                          unsigned frozen:1;
                     };
                   ...
                };
               ...
          };
     ...
     ...
       union {
             ...
             ...
             struct kmem_cache *slab_cache; /* SL[AU]B: Pointer to slab */
         };
    ...
    ...
};

freelist指针指向列表中的第一个自由对象。 每个自由对象由一个元数据区域组成,该区域包含指向列表中下一个自由对象的指针。 index保存第一个自由对象的元数据区域的偏移量(包含指向下一个自由对象的指针)。 最后一个自由对象的元数据区域将包含设置为 NULL 的下一个自由对象指针。 inuse包含已分配对象的总数,objects包含对象总数。 frozen是用作页面锁定的标志:如果页面已被 CPU 内核冻结,则只有该内核可以从该页面检索空闲对象。 slab_cache是指向当前使用此页面的 kmem 缓存的指针:

当分配请求到达时,第一个空闲对象通过freelist指针定位,并通过将其地址返回给调用者而从列表中删除。 inuse计数器也会递增,以指示已分配对象的数量增加。 然后,使用列表中下一个空闲对象的地址更新freelist指针。

为了实现增强的分配效率,为每个 CPU 分配了一个私有活动板列表,该列表包括针对每种对象类型的部分/空闲板列表。 这些片被称为 CPU 本地片,并由 structkmem_cache_cpu跟踪:

struct kmem_cache_cpu {
     void **freelist; /* Pointer to next available object */
     unsigned long tid; /* Globally unique transaction id */
     struct page *page; /* The slab from which we are allocating */
     struct page *partial; /* Partially allocated frozen slabs */
     #ifdef CONFIG_SLUB_STATS
        unsigned stat[NR_SLUB_STAT_ITEMS];
     #endif
};

当分配请求到达时,分配器采用快速路径并查看每个 CPU 缓存的freelist,然后返回空闲对象。 这称为快速路径,因为分配是通过不需要锁争用的中断安全原子指令执行的。 当快速路径失败时,分配器采用慢速路径,并按顺序查看 CPU 缓存的*page**partial*列表。 如果没有找到空闲对象,则分配器移动到节点的部分列表中;此操作要求分配器争用适当的排除锁。 失败时,分配器从伙伴系统中获得一个新的板条。 从节点列表获取或从伙伴系统获取新的平板被认为是非常慢的路径,因为这两个操作都不是确定性的。

下图描述了辅助数据结构和空闲列表之间的关系:

Vmalloc

页和片分配器都分配物理上连续的内存块,映射到连续的内核地址空间。 大多数情况下,内核服务和子系统更喜欢分配物理上连续的块,以利用缓存、地址转换和其他与性能相关的好处。 尽管如此,对非常大的块的分配请求可能会由于物理内存的碎片而失败,并且很少有情况需要分配大的块,例如支持动态加载模块、交换管理操作、大文件缓存等。

作为解决方案,内核提供了vmalloc,这是一个碎片化的内存分配器,它试图通过虚拟连续的地址空间连接物理上分散的内存区域来分配内存。 内核段内的一系列虚拟地址被保留用于 vmalloc 映射,称为 vmalloc 地址空间。 可以通过 vmalloc 接口映射的总内存取决于 vmalloc 地址空间的大小,该大小由特定于体系结构的内核宏VMALLOC_STARTVMALLOC_END定义;对于 x86-64 系统,vmalloc 地址空间的总范围是惊人的 32 TB**。** 然而,另一方面,这个范围对于大多数 32 位体系结构来说太小了(只有 120MB)。 最近的内核版本使用 vmalloc 范围来设置虚拟映射的内核堆栈(仅限 x86-64),我们在第一章中讨论了这一点。

以下是 vmalloc 分配和释放的接口例程:

/**
  * vmalloc  -  allocate virtually contiguous memory
  * @size:   -  allocation size
  * Allocate enough pages to cover @size from the page level
  * allocator and map them into contiguous kernel virtual space.
  *
  */
    void *vmalloc(unsigned long size) 
/**
  * vzalloc - allocate virtually contiguous memory with zero fill
1 * @size:  allocation size
  * Allocate enough pages to cover @size from the page level
  * allocator and map them into contiguous kernel virtual space.
  * The memory allocated is set to zero.
  *
  */
 void *vzalloc(unsigned long size) 
/**
  * vmalloc_user - allocate zeroed virtually contiguous memory for userspace
  * @size: allocation size
  * The resulting memory area is zeroed so it can be mapped to userspace
  * without leaking data.
  */
    void *vmalloc_user(unsigned long size) /**
  * vmalloc_node  -  allocate memory on a specific node
  * @size:          allocation size
  * @node:          numa node
  * Allocate enough pages to cover @size from the page level
  * allocator and map them into contiguous kernel virtual space.
  *
  */
    void *vmalloc_node(unsigned long size, int node) /**
  * vfree  -  release memory allocated by vmalloc()
  * @addr:          memory base address
  * Free the virtually continuous memory area starting at @addr, as
  * obtained from vmalloc(), vmalloc_32() or __vmalloc(). If @addr is
  * NULL, no operation is performed.
  */
 void vfree(const void *addr) /**
  * vfree_atomic  -  release memory allocated by vmalloc()
  * @addr:          memory base address
  * This one is just like vfree() but can be called in any atomic context except NMIs.
  */
    void vfree_atomic(const void *addr)

大多数内核开发人员避免分配 vmalloc 的原因是分配开销(因为这些开销不是身份映射的,需要特定的页表调整,从而导致 TLB 刷新)和访问操作期间涉及的性能损失。

连续内存分配器(CMA)

尽管开销很大,但虚拟映射分配在更大程度上解决了大内存分配的问题。 但是,有几种情况要求分配物理上连续的缓冲区。 DMA 传输就是这样一种情况。 设备驱动程序经常发现非常需要物理上连续的缓冲区分配(用于设置 DMA 传输),这是通过前面讨论的任何物理上连续的分配器来执行的。

然而,与特定类别的设备(如多媒体)打交道的驱动程序通常会发现自己在搜索巨大的连续内存块。 为此,多年来,这样的驱动程序一直通过内核参数mem在系统引导期间保留内存,该参数允许在引导时留出足够的连续内存,这些内存可以在驱动程序运行时重新映射到线性地址空间。 虽然这一策略很有价值,但也有其局限性:首先,当相应的设备没有启动访问操作时,这种保留的存储器暂时处于未使用状态;其次,根据要支持的设备的数量,保留的存储器的大小可能会大幅增加,这可能会由于物理存储器紧张而严重影响系统性能。

连续内存分配器(CMA)是为有效管理保留的内存而引入的内核机制。 CMA的关键是在分配器算法下引入保留的内存,这种内存称为CMA 区域。 CMA允许从CMA 区域分配设备和系统。 这是通过为保留存储器中的页面构建页面描述符列表并将其枚举到伙伴系统中来实现的,这使得能够通过页面分配器为常规需要(内核子系统)以及通过用于设备驱动程序的 DMA 分配例程分配CMA 页面

但是,必须确保 DMA 分配不会因为将CMA 页用于其他目的而失败,这是通过我们前面讨论过的migratetype属性来注意的。 将 CMA 列举到伙伴系统中的页面分配给MIGRATE_CMA属性,该属性指示页面是可移动的*。* 在为非 DMA 目的分配内存时*,页面分配器只能将 CMA 页面用于可移动分配(回想一下,此类分配可以通过__GFP_MOVABLE标志进行)。 当 DMA 分配请求到达时,内核分配持有的 CMA 页被移出保留区域(通过页迁移机制),从而获得可供设备驱动程序使用的内存。 此外,当为 DMA 分配页面时,它们的迁移类型*从MIGRATE_CMA变为MIGRATE_ISOLATE,使它们对伙伴系统不可见。

CMA 区域的大小可以在内核构建期间通过其配置接口选择;或者,也可以通过内核参数cma=传递。

简略的 / 概括的 / 简易判罪的 / 简易的

我们已经遍历了 Linux 内核最关键的方面之一,理解了内存表示和分配的各种细微差别。 通过理解这个子系统,我们也简洁地捕捉到了内核的设计敏锐性和实现效率,更重要的是,我们理解了内核在适应更精细、更新的启发式规则和机制以进行持续增强方面的动态性。 除了内存管理的细节之外,我们还衡量了内核在以最低成本最大化资源使用方面的效率,引入了所有经典的代码重用机制和模块化代码结构。

尽管内存管理的细节可能会因底层体系结构的不同而有所不同,但设计和实现风格的一般性基本保持不变,以实现代码稳定性和对更改的敏感度。

在下一章中,我们将进一步了解内核的另一个基本抽象:文件。 我们将深入了解文件 I/O,并探索其体系结构和实施细节。**