Linux下NUMA 内存管理初步理解

LINUX 系统在多核下,以及NUMA架构技术下 如何管理物理内存?

经过初步了解 发现系统对内存有以下工作

1 映射

2 内存碎片

3 内存回收

4 内存池

5 冷热页

6 水位线和保留内存

7 支持内存条热插拔

8 大页管理

这些功能给人感觉,尤其是DBA感觉是重新实现了数据缓存池的功能!

听说 LINUX 之父 非常讨厌数据库 ORACLE和MYSQL 直接IO 绕过系统缓存. 而PG就没有绕过!

林纳斯·本纳第克特·托瓦兹(Linus Benedict Torvalds,1969年12月28日- ),芬兰赫尔辛基人,著名的电脑程序员,Linux内核的发明人及该计划的合作者,毕业于赫尔辛基大学计算机系

实际上 并发如此! 系统对内存各种骚操作,并发是针对应用来的. 通过前面两篇NUMA架构文章来说, 系统对内存的操作是针对CPU来的.

LINUX 系统把物理内存按4K大小来划分管理单元 4K 一页. 类似MYSQL 16K 一个页.ORACLE 8KB 一个块!

LINUX 系统核心是STRUCT PAGE 你认可为C++的类, JAVA的类 4K页对应一个实例,对象. PAGE数组则是存放该对象的数组.

物理内存区域中管理的就是物理内存页( Linux 内存管理的最小单位),前面我们介绍的内核对物理内存的换入,换出,回收,内存映射等操作的单位就是页。内核为每一个物理内存区域分配了一个伙伴系统,用于管理该物理内存区域下所有物理内存页面的分配和释放。

那么系统支持 4KB,8KB,2MB,4MB 等大小的物理页面,它们都是 2 的整数次幂,为啥偏偏要选 4KB 呢?

在内存紧张的时候,内核会将不经常使用到的物理页面进行换入换出等操作,还有在内存与文件映射的场景下,都会涉及到与磁盘的交互,数据在磁盘中组织形式也是根据一个磁盘块一个磁盘块来管理的,4kB 和 4MB 都是磁盘块大小的整数倍,但在大多数情况下,内存与磁盘之间传输小块数据时会更加的高效,所以综上所述内核会采用 4KB 作为默认物理内存页大小。

假设我们有 4G 大小的物理内存,每个物理内存页大小为 4K,那么这 4G 的物理内存会被内核划分为 1M 个物理内存页,内核使用一个 struct page 的结构体来描述物理内存页,而每个 struct page 结构体占用内存大小为 40 字节,那么内核就需要用额外的 40 * 1M = 40M 的内存大小来描述物理内存页。

对于 4G 物理内存而言,这额外的 40M

内存占比相对较小,这个代价勉强可以接受,但是对内存锱铢必较的内核来说,还是会尽最大努力想尽一切办法来控制 struct page 结构体的大小。

因为对于 4G 的物理内存来说,内核就需要使用 1M 个物理页面来管理,1M 个物理页的数量已经是非常庞大的了,因此在后续的内核迭代中,对于 struct page 结构的任何微小改动,都可能导致用于管理物理内存页的 struct page 实例所需要的内存暴涨。

1 内核如何统一组织 NUMA 节点

内核中使用了 struct pglist_data 这样的一个数据结构来描述 NUMA 节点,2.4 之后的版本中使用了一个大小为 MAX_NUMNODES ,类型为 struct pglist_data 的全局数组 node_data[] 来管理所有的 NUMA 节点。

数组中每个下表存储PGLIST_DATA的变量的内存地址, 而该变量里面有存储了PAGE数组首地址; 下面是PgList_data结构体简化的内容

typedef struct pglist_data 
{// NUMA 节点idint node_id;// 指向 NUMA 节点内管理所有物理页 page 的数组struct page *node_mem_map;// NUMA 节点内第一个物理页的 pfnunsigned long node_start_pfn;// NUMA 节点内所有可用的物理页个数(不包含内存空洞)unsigned long node_present_pages;// NUMA 节点内所有的物理页个数(包含内存空洞)unsigned long node_spanned_pages; // 保证多进程可以并发安全的访问 NUMA 节点spinlock_t node_size_lock;.............// NUMA 节点中的物理内存区域个数int nr_zones; // NUMA 节点中的物理内存区域struct zone node_zones[MAX_NR_ZONES];// NUMA 节点的备用列表struct zonelist node_zonelists[MAX_ZONELISTS];        
}pg_data_t;

上面的NODE_MEM_MAP关联PAGE数组 4K物理内存页数组; NODE_ID 节点号, NODE_START_PFN是第一个物理页数组下标[1] 这类的 NR_ZONES: 映射区个数 NODE_ZONES 本地内存映射区数组 NODE_ZONELISTS 备用映射区数组,远端内存,其它节点的本地内存

NUMA 节点状态

每个节点有个状态,也用数组存在,状态根据上面的位图MASK来表示

enum node_states 
{N_POSSIBLE,  /* The node could become online at some point */N_ONLINE,  /* The node is online */N_NORMAL_MEMORY, /* The node has regular memory */
#ifdef CONFIG_HIGHMEMN_HIGH_MEMORY,  /* The node has regular or high memory */
#elseN_HIGH_MEMORY = N_NORMAL_MEMORY,
#endif#ifdef CONFIG_MOVABLE_NODEN_MEMORY,  /* The node has memory(regular, high, movable) */
#elseN_MEMORY = N_HIGH_MEMORY,
#endifN_CPU,  /* The node has one or more cpus */NR_NODE_STATES
};

相关视频推荐

90分钟了解Linux内存架构,numa的优势,slab的实现,vmalloc的原理

庞杂的内存问题,如何理出自己的思路出来,让你开发与面试双丰收

面对内存再不发怵,手把手带你实现内存池(自行准备linux环境)

Linux C/C++开发(后端/音视频/游戏/嵌入式/高性能网络/存储/基础架构/安全)

需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

2 映射

在 NUMA 架构下内存被划分成了一个一个的内存节点(NUMA Node),在每个 NUMA 节点中,内核又根据节点内物理内存的功能用途不同,将 NUMA 节点内的物理内存划分为四个物理内存区域分别是:ZONE_DMA, ZONE_DMA32, ZONE_NORMAL, ZONE_HIGHMEM。 其中 ZONE_MOVABLE 区域是逻辑上的划分,主要是为了防止内存碎片和支持内存的热插拔。

64位程序的虚拟地址空间需要映射到不同的区域里,图中的细节实际上是进程不同功能区域,这些区域需要映射到不同的内存里.就是上面的内存区域,分门别类后,方便后续的管理!

所以内核会根据各个物理内存区域的功能不同,将 NUMA 节点内的物理内存主要划分为以下四个物理内存区域:

ZONE_DMA:用于那些无法对全部物理内存进行寻址的硬件设备,进行 DMA 时的内存分配。例如前边介绍的 ISA 设备只能对物理内存的前 16M 进行寻址。该区域的长度依赖于具体的处理器类型。

ZONE_DMA32:与 ZONE_DMA 区域类似,该区域内的物理页面可用于执行 DMA 操作,不同之处在于该区域是提供给 32 位设备(只能寻址 4G 物理内存)执行 DMA 操作时使用的。该区域只在 64 位系统中起作用,因为只有在 64 位系统中才会专门为 32 位设备提供专门的 DMA 区域。

ZONE_NORMAL:这个区域的物理页都可以直接映射到内核中的虚拟内存,由于是线性映射,内核可以直接进行访问。

ZONE_HIGHMEM:这个区域包含的物理页就是我们说的高端内存,内核不能直接访问这些物理页,这些物理页需要动态映射进内核虚拟内存空间中(非线性映射)。该区域只在 32 位系统中才会存在,因为 64 位系统中的内核虚拟内存空间太大了(128T),都可以进行直接映射。

struct zone {// 防止并发访问该内存区域spinlock_t      lock;// 内存区域名称:Normal ,DMA,HighMemconst char      *name;// 指向该内存区域所属的 NUMA 节点struct pglist_data  *zone_pgdat;// 属于该内存区域中的第一个物理页 PFNunsigned long       zone_start_pfn;// 该内存区域中所有的物理页个数(包含内存空洞)unsigned long       spanned_pages;// 该内存区域所有可用的物理页个数(不包含内存空洞)unsigned long       present_pages;// 被伙伴系统所管理的物理页数atomic_long_t       managed_pages;// 伙伴系统的核心数据结构struct free_area    free_area[MAX_ORDER];// 该内存区域内存使用的统计信息atomic_long_t       vm_stat[NR_VM_ZONE_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;

ZONE是管理区域的(类,对象,实例)结构体数据,

____cacheline_internodealigned_in_smp

这是一个 Linux 内核中的宏定义,用于指定在 SMP 系统中缓存行对齐。具体来说,它表示将变量或数据结构填充到缓存行中,以避免多个 CPU 核心同时访问同一缓存行时的冲突和性能损失。"cacheline" 表示缓存行,"internode" 表示非本地节点,"aligned" 表示对齐,"in_smp" 表示在 SMP 系统中使用。

在ZONE结构体里面有个内存地址变量 *zone_pgdat 类型是 struct pglist_data PgList_data 里面有Zone变量的数组, 而Zone变量又反射回PgList_Data的内存地址(指针),这样回来读取PgList_data里面的物理页的数组

3 文件页和匿名页

从系统角度来看,存放在磁盘上的都是文件,文件装入内存后占用的内存页叫文件页. 其中文件是包含程序的,也就是说文件是指程序+数据!

匿名页是由进程(程序)执行过程中申请的内存 比如全局变量,进栈出栈,以及动态内存分配.

struct page {// 如果 page 为文件页的话,低位为0,指向 page 所在的 page cache// 如果 page 为匿名页的话,低位为1,指向其对应虚拟地址空间的匿名映射区 anon_vmastruct address_space *mapping;// 如果 page 为文件页的话,index 为 page 在 page cache 中的索引// 如果 page 为匿名页的话,表示匿名页在对应进程虚拟内存区域 VMA 中的偏移pgoff_t index; 
}

区分文件页和匿名页由PAGE结构体里面的 *mapping低为0和1 在内核中每个文件都会有一个属于自己的 page cache(页高速缓存),页高速缓存在内核中的结构体就是这个 struct address_space。它被文件的 inode 所持有。如果当前物理内存页 struct page 是一个文件页的话,那么 mapping 指针的最低位会被设置为 0 ,指向该内存页关联文件的 struct address_space(页高速缓存),pgoff_t index 字段表示该内存页 page 在页高速缓存 page cache 中的 index 索引。内核会利用这个 index 字段从 page cache 中查找该物理内存页,同时该 pgoff_t index 字段也表示该内存页中的文件数据在文件内部的偏移 offset。偏移单位为 page size。

如果当前物理内存页 struct page 是一个匿名页的话,那么 mapping 指针的最低位会被设置为 1 , 指向该匿名页在进程虚拟内存空间中的匿名映射区域 struct anon_vma 结构(每个匿名页对应唯一的 anon_vma 结构),用于物理内存到虚拟内存的反向映射

4 页高速缓存 PAGE_CACHE

当用户进程发起 read 系统调用之后,内核首先会在 page cache 中检查请求数据所在页面是否已经缓存在 page cache 中。

  • 如果缓存命中,内核直接会把 page cache 中缓存的磁盘文件数据拷贝到用户空间缓冲区中,从而避免了龟速的磁盘 IO。

  • 如果缓存没有命中,内核会分配一个物理页面,将这个新分配的页面插入 page cache 中,然后调度磁盘块 IO 驱动从磁盘中读取数据,最后用从磁盘中读取的数据填充这个物里页面。page cache 中缓存的不仅有基于文件的缓存页,还会缓存内存映射文件,以及磁盘块设备文件。

page cache 在内核中的数据结构是一个叫做 address_space 的结构体:struct address_space。这个名字起的真是有点词不达意

可以通过多个不同的进程打开一个相同的文件,进程每打开一个文件,内核就会为它创建 struct file 结构。这样在内核中就会有多个 struct file 结构来表示同一个文件,但是同一个文件的 page cache 也就是 struct address_space 在内核中只会有一个。

struct address_space 
{struct inode  *host;  // 关联 page cache 对应文件的 inodestruct radix_tree_root page_tree; // 这里就是 page cache。里边缓存了文件的所有缓存页面spinlock_t  tree_lock; // 访问 page_tree 时用到的自旋锁unsigned long  nrpages; // page cache 中缓存的页面总数..........省略..........const struct address_space_operations *a_ops; // 定义对 page cache 中缓存页的各种操作方法..........省略..........
}

struct inode *host :一个文件对应一个 page cache 结构 struct address_space ,文件的 inode 描述了一个文件的所有元信息。在 struct address_space 中通过 host 指针与文件的 inode 关联。而在 inode 结构体 struct inode 中又通过 i_mapping 指针与文件的 page cache 进行关联。 struct radix_tree_root page_tree : page cache 中缓存的所有文件页全部存储在 radix_tree 这样一个高效搜索树结构当中。在文件 IO 相关的操作中,内核需要频繁大量地在 page cache 中搜索请求页是否已经缓存在页高速缓存中,所以针对 page cache 的搜索操作必须是高效的,否则引入 page cache 所带来的性能提升将会被低效的搜索开销所抵消掉。

const struct address_space_operations *a_ops :a_ops 定义了 page cache 中所有针对缓存页的 IO 操作,提供了管理 page cache 的各种行为。比如:常用的页面读取操作 readPage() 以及页面写入操作 writePage() 等。保证了所有针对缓存页的 IO 操作必须是通过 page cache 进行的。page cache 中缓存的不仅仅是基于文件的页,它还会缓存内存映射页,以及磁盘块设备文件,况且基于文件的内存页背后也有不同的文件系统。所以内核只是通过 a_ops 定义了操作 page cache 缓存页 IO 的通用行为定义。而具体的实现需要各个具体的文件系统通过自己定义的 address_space_operations 来描述自己如何与 page cache 进行交互。

下面是自定义操作函数结构体.相当于类的方法

struct address_space_operations {// 写入更新页面缓存int (*writepage)(struct page *page, struct writeback_control *wbc);// 读取页面缓存int (*readpage)(struct file *, struct page *);// 设置缓存页为脏页,等待后续内核回写磁盘int (*set_page_dirty)(struct page *page);// Direct IO 绕过 page cache 直接操作磁盘ssize_t (*direct_IO)(struct kiocb *, struct iov_iter *iter);........省略..........
}

缓存页的树型结构体

struct radix_tree_root 
{gfp_t   gfp_mask;struct radix_tree_node __rcu *rnode;  // radix_tree 根节点
};struct address_space
{
// 这里就是 page cache。里边缓存了文件的所有缓存页面struct radix_tree_root  page_tree; ..........省略..........
}struct radix_tree_node 
{void __rcu *slots[RADIX_TREE_MAP_SIZE]; //包含 64 个指针的数组。用于指向下一层节点或者缓存页unsigned char offset; //父节点中指向该节点的指针在父节点 slots 数组中的偏移unsigned char count;//记录当前节点的 slots 数组指向了多少个节点struct radix_tree_node *parent; // 父节点指针struct radix_tree_root *root; // 根节点..........省略.........// radix_tree 中的二维标记数组,用于标记子节点的状态。unsigned long tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS];
};

最后跟MYSQL B树差不多样

radix_tree 深度可以缓存多大的文件内容:

radix_tree 深度

page 最大索引值

缓存文件大小

1

2^6 - 1 = 63

256K

2

2^12 - 1 = 4095

16M

3

2^18 - 1 = 262143

1G

4

2^24 -1 =16777215

64G

5

2^30 - 1

4T

6

2^36 - 1

64T

在 radix_tree 是根据缓存页的 index (索引)来组织管理缓存页的,

内核会根据这个 index 迅速找到对应的缓存页。

在缓存页描述符 struct page 结构中保存了其在 page cache 中的索引 index。

struct page 
{unsigned long flags;  //缓存页标记struct address_space *mapping; // 缓存页所在的 page cacheunsigned long index;  // 页索引...  
} 

如果一颗 radix_tree 的深度为 2(不包括叶子节点),那么它就可以缓存 64 * 64 = 4096 个文件页,表示的索引范围为 0 - 4095,在这种情况下,缓存页索引 offset 的低 12 位可以分成 两个 6 位的字段,高位的字段用来表示第一层节点的 slots 数组的下标,低位字段用于表示第二层节点的 slots 数组下标。

这段话大意就是索引表达,通过位来实现 下面是两个函数的查找算法,大概瞄一眼就行

static inline struct page *find_get_page(struct address_space *mapping, pgoff_t offset)
{  //offset 是page cache 中的索引 indexreturn pagecache_get_page(mapping, offset, 0, 0);
}struct page *pagecache_get_page(struct address_space *mapping, pgoff_t offset, int fgp_flags, gfp_t gfp_mask)
{struct page *page;repeat:// 在 radix_tree 中根据 缓存页 offset 查找缓存页page = find_get_entry(mapping, offset);// 缓存页不存在的话,跳转到 no_page 处理逻辑if (!page)goto no_page;.......省略.......
no_page:if (!page && (fgp_flags & FGP_CREAT)) { // 分配新页page = __page_cache_alloc(gfp_mask);if (!page)return NULL;if (fgp_flags & FGP_ACCESSED)//增加页的引用计数__SetPageReferenced(page);// 将新分配的内存页加入到页高速缓存 page cache 中err = add_to_page_cache_lru(page, mapping, offset, gfp_mask);.......省略.......}return page;
}
  • 内核首先调用 find_get_entry 方法根据缓存页的 offset 到 page cache 中去查找看请求的文件页是否已经在页高速缓存中。如果存在直接返回。

  • 如果请求的文件页不在 page cache 中,内核则会首先会在物理内存中分配一个内存页,然后将新分配的内存页加入到 page cache 中,并增加页引用计数。

  • 随后会通过 address_space_operations 重定义的 readpage 激活块设备驱动从磁盘中读取请求数据,然后用读取到的数据填充新分配的内存页。

下面这个是结构体变量,同时赋值操作,不过这里是赋的是函数. 类似与C++构造函数, C语言中函数类型内存地址变量(指针)绑定函数名,就可以通过内存地址变量调用函数了

static const struct address_space_operations ext4_aops = 
{.readpage       = ext4_readpage,.writepage      = ext4_writepage,.direct_IO      = ext4_direct_IO,........省略.....
};

4.2 缓存中的脏页标记

快速查找 page cache 中的所有脏页。但是如果此时 page cache 中的大部分缓存页都不是脏页,那么顺序遍历 radix_tree 的方式就实在是太慢了,所以为了快速搜索到脏页,就需要在 radix_tree 中的每个节点radix_tree_node中加入一个针对其所有子节点的脏页标记,如果其中一个子节点被标记被脏时,那么这个子节点对应的父节点 radix_tree_node 结构中的对应脏页标记位就会被置 1 。

 struct radix_tree_node // radix_tree 中的二维标记数组,用于标记子节点的状态。unsigned long tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS];

tags 二维数组。其中第一维 tags[] 用来表示标记类型,有多少标记类型,数组大小就为多少,比如 tags[0] 表示 PG_dirty 标记数组,tags[1] 表示 PG_writeback 标记数组。

最后 这样的结构体关系图:

5 NUMA 节点中的文件页和匿名页回收 LRU

系统使用4条LRU链条管理

文件页第一次读取的时候存放在INACTIVE 冷链条头部,如果再被读取一次就提升到ACTIVE 热链尾部.

匿名页 第一次读取存在 热链的尾巴, 当匿名页再次被访问的时候就会被被提升到 active 链表的头部。

当遇到内存紧张的情况需要换页时,内核会从 active 链表的尾部开始扫描,将一定量的页面降级到 inactive 链表头部,这样一来原来位于 inactive 链表尾部的页面就会被置换出去。

当内存紧张的时候,内核就会优先将 inactive 链表中的内存页置换出去。

内核在回收内存的时候,这两个列表中的回收优先级为:

inactive 链表尾部 > inactive 链表头部 > active 链表尾部 > active 链表头部。

swappiness 用于表示 Swap 机制的积极程度,数值越大,Swap 的积极程度,越高越倾向于回收匿名页。数值越小,Swap 的积极程度越低,越倾向于回收文件页

struct page 
{
struct list_head lru; //属性就是用来指向物理页被放置在了哪个链表上。   
atomic_t _refcount; //该物理页的次数
unsigned long flags; //物理内存页属性和状态的标志位 flag物理内存页属性和状态的标志位 flag}

FLAGS 高八位,低位FLAGS

下面是页的FLAGS 在CAT/PROC/MEMINFO 也见到一二

enum pageflags 
{PG_locked,  /* Page is locked. Don't touch. */PG_referenced,PG_uptodate,PG_dirty,PG_lru,PG_active,PG_slab,PG_reserved,PG_compound,PG_private,  PG_writeback,  PG_reclaim,  
#ifdef CONFIG_MMUPG_mlocked,  /* Page is vma mlocked */PG_swapcache = PG_owner_priv_1, ................
}

6 NUMA 节点中的内存回收

一般达到了内存压力大的时候系统会触发KSWAPD进程起来工作,把匿名页SWAP到磁盘上,继续紧张下去就触发更重要的内存进行 回收

涉及以下几个相关数据结构

typedef struct pglist_data 
{.........// 页面回收进程struct task_struct *kswapd;wait_queue_head_t kswapd_wait;// 内存规整进程struct task_struct *kcompactd;wait_queue_head_t kcompactd_wait;..........
} pg_data_t;struct zone 
{// 物理内存区域中的水位线unsigned long _watermark[NR_WMARK];// 优化内存碎片对内存分配的影响,可以动态改变内存区域的基准水位线。unsigned long watermark_boost;} ____cacheline_internodealigned_in_smp;enum zone_watermarks 
{WMARK_MIN,WMARK_LOW,WMARK_HIGH,NR_WMARK
};#define min_wmark_pages(z) (z->_watermark[WMARK_MIN] + z->watermark_boost)
#define low_wmark_pages(z) (z->_watermark[WMARK_LOW] + z->watermark_boost)
#define high_wmark_pages(z) (z->_watermark[WMARK_HIGH] + z->watermark_boost)

也就是我们老三样其中的 zone 类和pglist_data类 (结构体)类型

7 NUMA 节点中的ZONE的水位线和保留内存

7.1 物理内存区域中的水位线

内核会为每个 NUMA 节点中的每个物理内存区域定制三条用于指示内存容量的水位线,分别是:WMARK_MIN(页最小阈值), WMARK_LOW (页低阈值),WMARK_HIGH(页高阈值)。这三条水位线定义在 /include/linux/mmzone.h 文件中:

enum zone_watermarks{WMARK_MIN,WMARK_LOW,WMARK_HIGH,NR_WMARK
};struct zone 
{// 物理内存区域中的水位线unsigned long _watermark[NR_WMARK];// 优化内存碎片对内存分配的影响,可以动态改变内存区域的基准水位线。unsigned long watermark_boost;} ____cacheline_internodealigned_in_smp;

这三条水位线对应的 watermark 数值存储在每个物理内存区域 struct zone 结构中的 _watermark[NR_WMARK] 数组中。

cat /proc/zoneinfo

水位线:

  • free 就是该物理内存区域内剩余的内存页数,它的值和后面的 nr_free_pages 相同。

  • min、low、high 就是上面提到的三条内存水位线:_watermark[WMARK_MIN],_watermark[WMARK_LOW] ,_watermark[WMARK_HIGH]。

  • nr_zone_active_anon 和 nr_zone_inactive_anon 分别是该内存区域内活跃和非活跃的匿名页数量。

  • nr_zone_active_file 和 nr_zone_inactive_file 分别是该内存区域内活跃和非活跃的文件页数量。

WMARK_MIN,WMARK_LOW ,WMARK_HIGH 这三个水位线的数值是通过内核参数 /proc/sys/vm/min_free_kbytes 为基准分别计算出来的

通常情况下 WMARK_LOW 的值是 WMARK_MIN 的 1.25 倍,WMARK_HIGH 的值是 WMARK_LOW 的 1.5 倍。

而 WMARK_MIN 的数值就是由这个内核参数 min_free_kbytes 来决定的。

7.2 预留内存

每个物理内存区域 struct zone 还为操作系统预留了一部分内存,这部分预留的物理内存用于内核的一些核心操作,这些操作无论如何是不允许内存分配失败的。内核中关于内存分配的场景无外乎有两种方式:

  1. 当进程请求内核分配内存时,如果此时内存比较充裕,那么进程的请求会被立刻满足,如果此时内存已经比较紧张,内核就需要将一部分不经常使用的内存进行回收,从而腾出一部分内存满足进程的内存分配的请求,在这个回收内存的过程中,进程会一直阻塞等待。

  2. 另一种内存分配场景,进程是不允许阻塞的,内存分配的请求必须马上得到满足,比如执行中断处理程序或者执行持有自旋锁等临界区内的代码时,进程就不允许睡眠,因为中断程序无法被重新调度。这时就需要内核提前为这些核心操作预留一部分内存,当内存紧张时,可以使用这部分预留的内存给这些操作分配。

struct zone 
{
//表示的是该内存区域内预留内存的大小,范围为 128 到 65536 KB 之间。unsigned long nr_reserved_highatomic;long lowmem_reserve[MAX_NR_ZONES];
//数组则是用于规定每个内存区域必须为自己保留的物理页数量
}

一些用于特定功能的物理内存必须从特定的内存区域中进行分配,比如外设的 DMA 控制器就必须从 ZONE_DMA 或者 ZONE_DMA32 中分配内存。

但是一些用于常规用途的物理内存则可以从多个物理内存区域中进行分配,当 ZONE_HIGHMEM 区域中的内存不足时,内核可以从 ZONE_NORMAL 进行内存分配,ZONE_NORMAL 区域内存不足时可以进一步降级到 ZONE_DMA 区域进行分配。

而低位内存区域中的内存总是宝贵的,内核肯定希望这些用于常规用途的物理内存从常规内存区域中进行分配,这样能够节省 ZONE_DMA 区域中的物理内存保证 DMA 操作的内存使用需求,但是如果内存很紧张了,高位内存区域中的物理内存不够用了,那么内核就会去占用挤压其他内存区域中的物理内存从而满足内存分配的需求。

但是内核又不会允许高位内存区域对低位内存区域的无限制挤压占用,因为毕竟低位内存区域有它特定的用途,所以每个内存区域会给自己预留一定的内存,防止被高位内存区域挤压占用。而每个内存区域为自己预留的这部分内存就存储在 lowmem_reserve 数组中。每个内存区域是按照一定的比例来计算自己的预留内存的,这个比例我们可以通过 cat /proc/sys/vm/lowmem_reserve_ratio 命令查看从左到右分别代表了 ZONE_DMA,ZONE_DMA32,ZONE_NORMAL,ZONE_MOVABLE,ZONE_DEVICE 物理内存区域的预留内存比例。服务器是 64 位,所以没有 ZONE_HIGHMEM 区域。

以 ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM 这三个物理内存区域举例,它们的 lowmem_reserve_ratio 分别为 256,32,0。它们的大小分别是:8M,64M,256M,按照每页大小 4K 计算它们区域里包含的物理页个数分别为:2048, 16384, 65536。

lowmem_reserve_ratio

内存区域大小

物理内存页个数

ZONE_DMA

256

8M

2048

ZONE_NORMAL

32

64M

16384

ZONE_HIGHMEM

0

256M

65536

ZONE_DMA 为防止被 ZONE_NORMAL 挤压侵占,而为自己预留的物理内存页为:16384 / 256 = 64。

ZONE_NORMAL 为防止被 ZONE_HIGHMEM 挤压侵占而为自己预留的物理内存页为:65536 / 32 = 2048。

上面两个算法有点绕, 我也没有研究清楚

各个内存区域为防止被高位内存区域过度挤压占用,而为自己预留的内存大小,

我们可以通过前边 cat /proc/zoneinfo 命令来查看,输出信息的

protection:则表示各个内存区域预留内存大小。

8 NUMA 节点中的内存冷热页

在 NUMA 内存架构下,这些 NUMA 节点中的物理内存区域 zone 管理的这些物理内存页,哪些是在 CPU 的高速缓存中?哪些又不在 CPU 的高速缓存中呢?内核如何来管理这些加载进 CPU 高速缓存中的物理内存页呢?

冷热页使用2个结构体数据表达

因为每个 CPU 都有自己独立的高速缓存,所以每个 CPU 对应一个 per_cpu_pages 结构,在内核版本 2.6.25 之后,将冷页和热页的管理合并在了一个列表中,热页放在列表的头部,冷页放在列表的尾部。

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[NR_PCP_LISTS];
};

struct zone 
{struct per_cpu_pages __percpu *per_cpu_pageset;int pageset_high;int pageset_batch;} ____cacheline_internodealigned_in_smp;

9 大页 compound_page

Linux 内存管理架构中都是统一通过 struct page 来管理内存,而巨型大页却是通过两个或者多个物理上连续的内存页 page 组装成的一个比普通内存页 page 更大的页,那么巨型页的管理与普通页的管理如何统一?

   struct page {      
// 首页 page 中的 flags 会被设置为 PG_head 表示复合页的第一页unsigned long flags; // 其余尾页会通过该字段指向首页unsigned long compound_head;   
// 用于释放复合页的析构函数,保存在首页中unsigned char compound_dtor;
// 该复合页有多少个 page 组成,order 还是分配阶的概念,首页中保存// 本例中的 order = 2 表示由 4 个普通页组成unsigned char compound_order;
// 该复合页被多少个进程使用,内存页反向映射的概念,首页中保存atomic_t compound_mapcount;
// 复合页使用计数,首页中保存atomic_t compound_pincount;}

首页中还保存关于复合页的一些额外信息,比如用于释放复合页的析构函数会保存在首页 struct page 结构里的 compound_dtor 字段中,复合页的分配阶 order 会保存在首页中的 compound_order 中,以及用于指示复合页的引用计数 compound_pincount,以及复合页的反向映射个数(该复合页被多少个进程的页表所映射)compound_mapcount 均在首页中保存。

复合页中的所有尾页都会通过其对应的 struct page 结构中的 compound_head 指向首页,这样通过首页和尾页就组装成了一个完整的复合页 compound_page 。

10 内存分配器

linux用两个内存分配器分配内存 分别是伙伴系统 buddy和SLAB.

BUDDY 在每个ZONE里面分配4K页内存,而SLAB是在4KB页里面分配更小的内存!

slab 就好比一个对象池,内核中的数据结构对象都对应于一个 slab 对象池,用于分配这些固定类型对象所需要的内存。

它的基本原理是从伙伴系统中申请一整页内存,然后划分成多个大小相等的小块内存被 slab 所管理。这样一来 slab 就和物理内存页 page 发生了关联,由于 slab 管理的单元是物理内存页 page 内进一步划分出来的小块内存,所以当 page 被分配给相应 slab 结构之后,struct page 里也会存放 slab 相关的一些管理数据。下面PAGE有些复杂了

struct page {struct {    /* slab, slob and slub */union {struct list_head slab_list;struct {    /* Partial pages */struct page *next;
#ifdef CONFIG_64BITint pages;  /* Nr of pages left */int pobjects;   /* Approximate count */
#elseshort int pages;short int pobjects;
#endif};};struct kmem_cache *slab_cache; /* not slob *//* Double-word boundary */void *freelist;     /* first free object */union {void *s_mem;    /* slab: first object */struct {            /* SLUB */unsigned inuse:16;unsigned objects:15;unsigned frozen:1;};};};}
  • struct list_head slab_list :slab 的管理结构中有众多用于管理 page 的链表,比如:完全空闲的 page 链表,完全分配的 page 链表,部分分配的 page 链表,slab_list 用于指定当前 page 位于 slab 中的哪个具体链表上。

  • struct page *next : 当 page 位于 slab 结构中的某个管理链表上时,next 指针用于指向链表中的下一个 page。

  • int pages : 表示 slab 中总共拥有的 page 个数。

  • int pobjects : 表示 slab 中拥有的特定类型的对象个数。

  • struct kmem_cache *slab_cache : 用于指向当前 page 所属的 slab 管理结构,通过 slab_cache 将 page 和 slab 关联起来。

  • void *freelist : 指向 page 中的第一个未分配出去的空闲对象,前面介绍过,slab 向伙伴系统申请一个或者多个 page,并将一整页 page 划分出多个大小相等的内存块,用于存储特定类型的对象。

  • void *s_mem : 指向 page 中的第一个对象。

  • unsigned inuse : 表示 slab 中已经被分配出去的对象个数,当该值为 0 时,表示 slab 中所管理的对象全都是空闲的,当所有的空闲对象达到一定数目,该 slab 就会被伙伴系统回收掉。

  • unsigned objects : slab 中所有的对象个数。

  • unsigned frozen : 当前内存页 page 被 slab 放置在 CPU 本地缓存列表中,frozen = 1,否则 frozen = 0 。

最后每个NODE 管理每个ZONE ,

每个ZONE管理PAGES或者SECTION,

每个SECTION管理连续的PAGES

其中三大数据结构 STRUCT PAGE, STRUCT ZONE,STRUCT PGLIST_DATA

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/600023.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

EG网关网口连接触控一体机操作说明V1.0 20210809

前言:工业现场中,很多情况下都会使用触摸屏连接PLC这一套设备,此一体机是将触摸屏和PLC两款设备集成到一起,可以简化现场设备结构,方便维护。同时,使用蓝蜂 EG系列网关,可以实现远程在线监控数据…

代码随想录算法训练营第十五天| 二叉树 513. 找树左下角的值 112. 路径总和 106.从中序与后序遍历序列构造二叉树

513. 找树左下角的值 层序遍历 本题用层序遍历可以直接秒了&#xff0c;直接提取每一层中最左边的元素&#xff08;i0&#xff09;&#xff0c;然后保存到最后一层即可。 class Solution { public:int findBottomLeftValue(TreeNode* root) {queue<TreeNode*> que;int…

Java学习苦旅(十七)——栈和队列

本篇博客将详细讲解Java中的栈和队列。 文章目录 栈概念Java中Stack常用方法代码实现 队列概念队列常用方法对比QueueDeque 代码实现Queue 结尾 栈 概念 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一…

惊呆:RocketMQ顺序消息,是“4把锁”实现的(顺序消费)

尼恩说在前面 在40岁老架构师 尼恩的读者交流群(50)中&#xff0c;最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格&#xff0c;遇到很多关于RocketMQ 的、很重要的面试题&#xff1a; 如何保证RocketMQ消息有序&#xff1f; Ro…

应用层网络协议

tags: [“计算机网络”] descripution: “学习应用层的一些常用协议” 网络协议&#xff1a;约定的信息传输的格式&#xff0c;如几个字节是消息头、消息头记录什么信息之类的&#xff1b;c/s架构&#xff1a;不一定是两台计算机&#xff0c;而是两个应用、两个端口工具&#…

poium测试库之JavaScript API封装原理

为什么要封装JavaScript的API&#xff1f; 因为有些场景下Selenium提供的API并不能满足我们需求。比如&#xff0c;滑动浏览滚动条&#xff0c;控制元素的显示/隐藏&#xff0c;日历控件的操作等&#xff0c;都可以通过JavaScrip实现&#xff0c;而且Selenium为我们提供了 exe…

教你直接在 Midjourney 网站上创建图像

经过一年多的工作和使用 Midjourney 的乐趣&#xff0c;我们终于能够直接在网站上生成图像。许多以前没有使用过 Discord 的人对于只能在 Discord 上创建图像并不满意。现在&#xff0c;几乎所有人的等待终于结束了。 直接在 Midjourney 网站上生成图像 David Holz 在 Discor…

SSM电影售票管理系统----计算机毕业设计

项目介绍 管理员角色包含以下功能&#xff1a; 管理员登陆,管理员用户管理,新闻公告增删改查,电影类型增删改查,影院信息增删改查,电影信息增删改查,订单查询,电影评价管理等功能。 用户角色包含以下功能&#xff1a; 用户首页,用户登录,查看电影详情,加入购物车,下单电影票,…

数据结构:图详解

图的存储方式 邻接矩阵 首先先创建图&#xff0c;这一个我们可以使用邻接矩阵或者邻接链 表来进行存储&#xff0c;我们要实现的无向图的创建&#xff0c;我们先创建 一个矩阵尺寸为n*n&#xff0c;n为图中的节点个数如图所示 可以看出图中有5个结点&#xff0c;那我们创建…

基于web3.js和ganache实现智能合约调用

目的&#xff1a;智能合约发布到本地以太坊模拟软件ganache并完成交互 准备工作&#xff1a; web3.jsganache模拟软件 ganache参数配置 从ganache获取一个url&#xff0c;和一个账号的地址&#xff0c; url直接使用图中的rpc server位置的数据即可 账号address从下列0x开头…

SpringBoot中动态注册接口

1. 说明 接口注册&#xff0c;使用RequestMappingHandlerMapping来实现mybatis中动态执行sql使用github上的SqlMapper工具类实现 2. 核心代码片段 以下代码为spring动态注册接口代码示例 Autowired private RequestMappingHandlerMapping requestMappingHandlerMapping;publ…

进程控制-操作系统

1. 进程概述 进程和程序的区别:程序和进程是两个不同的概念&#xff0c;他们的状态&#xff0c;占用的系统资源都是不同的。 程序&#xff1a;就是磁盘上的可执行文件文件, 并且只占用磁盘上的空间&#xff0c;是一个静态的概念。进程&#xff1a;被执行之后的程序叫做进程&a…

Maven之依赖的传递

问题导入 1. 依赖传递 A依赖B&#xff0c;B依赖C&#xff0c;A是否依赖于C呢&#xff1f;–A依赖于C 依赖具有传递性 路径优先&#xff1a;当依赖中出现相同的的资源时&#xff0c;层级越深&#xff0c;优先级越低&#xff0c;层级越浅&#xff0c;优先级越高 声明优先&…

nacos 2.* 部署在linux服务器无法注册问题

通过sdk注册代码 报错 Exception in thread "main" ErrCode:-401, ErrMsg:Client not connected, current status:STARTING at com.alibaba.nacos.common.remote.client.RpcClient.request(RpcClient.java:639) at com.alibaba.nacos.common.remote.client…

【网络编程】——基于TCP协议实现回显服务器及客户端

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【网络编程】【Java系列】 本专栏旨在分享学习网络编程的一点学习心得&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 一、TCP实…

AI无人直播系统怎么样?三点说明

近年来&#xff0c;因为科技的高速进步&#xff0c;不断涌现出了越来越多的新技术和创新事物&#xff0c;它们以其独特的方式取代了我们的许多传统做法&#xff0c;从而彻底解放了我们的双手。在这股潮流中&#xff0c;无人直播作为一种创新形式&#xff0c;使得直播变得更加简…

【容器】K8s RBAC介绍

认识RBAC RBAC&#xff08;基于角色的访问控制&#xff09;是一种将权限分配给用户和服务的方法&#xff0c;基于他们的角色来确定他们可以访问和修改的资源。K8s使用RBAC作为来访请求鉴权的机制之一。 场景&#xff1a;访问K8s接口时的认证和鉴权 某些场景下&#xff0c;我…

面试算法98:路径的数目

题目 一个机器人从mn的格子的左上角出发&#xff0c;它每步要么向下要么向右&#xff0c;直到抵达格子的右下角。请计算机器人从左上角到达右下角的路径的数目。例如&#xff0c;如果格子的大小是33&#xff0c;那么机器人从左上角到达右下角有6条符合条件的不同路径。 分析…

rabbitmq延时队列相关配置

确保 RabbitMQ 的延时消息插件已经安装和启用。你可以通过执行以下命令来安装该插件&#xff1a; rabbitmq-plugins enable rabbitmq_delayed_message_exchange 如果提示未安装&#xff0c;以下是安装流程&#xff1a; 查看mq版本&#xff1a; 查看自己使用的 MQ&#xff08;…

全网最全丨傻瓜式Fiddler教程大全丨从安装到抓包

前言 在我们做接口测试的时候&#xff0c;经常需要验证发送的消息是否正确&#xff0c;或者在出现问题的时候&#xff0c;查看手机客户端发送给server端的包内容是否正确&#xff0c;就需要用到抓包工具。 今天&#xff0c;给大家带来最常用的Fiddler的傻瓜式教程大全——从安…