linux内核memory model

news2024/11/24 7:05:17

一、背景
  操作系统的一个重要功能就是管理计算机中的各种硬件资源,比如说CPU、内存、显存、串口等,其中内存是这些资源中很珍贵的部分,所以为了能把内存管理好做了很多工作。硬件内存架构是不停地发展变化的,内核中管理内存的模型也在跟着硬件的变化而不停地演进。当前linux内核中有三种模型,分别是:flat memory model,Discontiguous memory model和sparse memory model。本文的参考代码是linux5.0,为了清晰地分析内存模型,先介绍相关的一些基本概念,具体如下:

1、什么是PFN:
  linux为了方便把物理内存管理起来,内核中把物理内存在逻辑上划分为一块块大小为page size的page frame,同时每个page frame都对应一个编号,称为PFN。其中page size是内核的配置以及硬件体系结构决定的,比如常见的page size为4K。根据PFN就可以算出这个page frame对应的物理地址,这样CPU就能通过地址总线访问这些物理页了。

  怎么计算物理地址的PFN呢,物理地址和PFN是一一对应的,比如page size为4K时,把物理地址左移12位就得到了PFN,可参考如下代码:

include/linux/pfn.h
#define PFN_PHYS(x)	((phys_addr_t)(x) << PAGE_SHIFT)
#define PHYS_PFN(x)	((unsigned long)((x) >> PAGE_SHIFT))

2、什么是struct page:
  在逻辑上把物理内存划分好后,为了跟踪每个page frame的状态,linux内核为每一个page frame配备了一个"struct page",用于跟踪对应物理页的状态,比如区分具体物理页保存的是进程的数据,还是内核的代码段或数据段。同样还得区分物理页是否是空闲的,如果物理页不存放任何有用的数据,那么它就是空闲的,当它用于存放进程数据、充当cache时,它就不是空闲状态。struct page会把物理页的各种信息都保存起来。

3、为什么要有内存模型了:
  有了PFN和struct page,还得把他们俩联系起来了,这就是pfn_to_page()与page_to_pfn()的工作,意思很明显了,PFN和page之间的相互转换就涉及到了内存模型。

  那为什么又是三种了,计算机刚起步的时候,物理内存都是连续的一整块,而且地址一般都是从0开始的,同时对于每个processor来说都一样,那时候生活是简单且美好的,只需要根据物理内存的大小分配一个struct page的数组,数组的index就是对应的PFN,看起一切都是那么美好。但时代在变化,计算机系统更是日新月异,内存早已不是那个简单单纯的它了,物理地址不一定从0开始了,也不一定连续了,内存中间可能有空洞了,而且还支持热插拔了。所以随着时间的推移,出现三种内存模型来应对硬件上的变化,下面详细地介绍每一个具体的模型。

二、flat memory model
  这是最简单的内存模型,所管理的物理内存就是连续的,没有空洞的,同时对于每个CPU来说都是一样的。在linux内核使用此模型时,会分配一个全局的struct page的数组mem_map,每个数组元素对应一个page frame,PFN和数组的下标是一种线性的对应关系,主要与ARCH_PFN_OFFSET有关,如果正好ARCH_PFN_OFFSET为0,数组的index就和PFN一一对应起来了,如下图所示:
在这里插入图片描述
  其实mem_map对应的数组是根据实际的物理内存大小动态分配,在flat memory model模型下,pfn_to_page()与page_to_pfn()的实现:

include/asm-generic/memory_model.h

#if defined(CONFIG_FLATMEM)
#define __pfn_to_page(pfn)	(mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page)	((unsigned long)((page) - mem_map) + \
				 ARCH_PFN_OFFSET)

二、Discontiguous memory model
  时间回溯到1999年,为了linux能在NUMA的硬件机器上更好地运行,做了很多工作,DISCONTIGMEM model就是其中部分成果,它就是用于处理不连续的物理内存。DISCONTIGMEM model引入了memory node的概念,同时memory node是NUMA内存管理的基础。从内存管理的角度来说,每个memory node都拥有一个独立的内存管理子系统,比如都有空闲页列表,使用页列表,最近最少访问的信息以及页使用的一些统计数据。在linux中,memory node是用struct pglist_data来表示,其中包含了此node特有的memory map。DISCONTIGMEM model前提是要求每个memory node内的内存在物理上是连续的,struct pglist_data中包含了一个类似flat memory map的struct page数组,从memory node内部来看,PFN和page之的转换与flat的一样,这样就完美地解决了内存不连续的问题。

  DISCONTIGMEM model必须要提供一种方式,即把包含在memory node中内存的PFN转换为相应的struct page,同样,它也要能根据struct page得到相应的PFN。当要把PFN转换为struct page时,首先从PFN中获取到memory node,然后通过"PFN - NODE_DATA(nid)->node_start_pfn"得到PFN在具体memory node中的page_offset,再根据"NODE_DATA(nid)->node_mem_map + page_offset"获得PFN对应的struct page;struct page中的flag中包含了memory node信息,故从struct page转换为PFN就简单了,请参考具体代码,不在赘述。

  DISCONTIGMEM model有个致命的弱点,它无法支持内存的热插拔。NUMA node的内存数量级对于支持热插拔来说太大了,但拆分NUMA node的话可能又会增加很多内存碎片和开销。每个NUMA node都有自己独立的内存管理子系统,对应的就是每个子系统都有相应的开销,拆分NUMA node会进一步加剧这些开销,这也是DISCONTIGMEM model无法支持热插拔的原因。DISCONTIGMEM model和NUMA model这两者本质上是不同的,但DISTONCTIGMEM是NUMA model的部分成果出现的,这种天然的关系导致两者之间的耦合度太高了。

  以下是假设有三个memory node对应的DISTONTIGMEM model的示意图
在这里插入图片描述
如下是DISCONTIGMEM model对应的代码

#define __pfn_to_page(pfn)			\
({	unsigned long __pfn = (pfn);		\
	unsigned long __nid = arch_pfn_to_nid(__pfn);  \
	NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);\
})

#define __page_to_pfn(pg)						\
({	const struct page *__pg = (pg);					\
	struct pglist_data *__pgdat = NODE_DATA(page_to_nid(__pg));	\
	(unsigned long)(__pg - __pgdat->node_mem_map) +			\
	 __pgdat->node_start_pfn;					\
})

三、SPARSEMEM model
  计算机的硬件在不停的更新,物理内存的架构也在不断发展,linux内核的Memory model也要进行演化,使它能够更好地管理物理内存。在linux刚开始的时候,物理内存是起始物理地址为0的连续的、简单的线性序列,大小也就几个M。每个物理内存页,在mem_map数组中都对应一个条目,数组的index就对应物理页的PFN。后来出现了NUMA机器,也出现不连续的物理内存,伴随着也就出现了DISCONTIGMEM model。当前内存又开始支持热插拔了,且memory node中的内存也不一定连续了,DISCONTIGMEM model又无法满足这些情况了,SPARSEMEM model就涌现出来了。

  在物理内存不连续的机器上,SPARSSEMEM希望最终能够替代DISCONTIGMEM。相对于DISCONTIGMEM来说,SPARSEMEM与CONFIG_NUMA是完全隔离的,NUMA与DISCONTIGMEM是耦合在一起的。SPARSEMEM还有另外一个优点,它也不要求NUMA node中的内存一定是连续的。

  SPARSEMEM用一系列的section集合来抽象内存,其中section的大小是由具体的体系结构定义的,一般是1G,在linux内核中,用struct mem_section表示一个section。SPARSEMEM的section集合有两种方式管理,一种是静态方式,即静态定义了一个二维数组,用于管理section集合;另外一种是运行时动态分配内存的方式,这是通过是否定义定义CONFIG_SPARSEMEM_EXTREME来决定的,如下:

#ifdef CONFIG_SPARSEMEM_EXTREME
struct mem_section **mem_section;
#else
struct mem_section mem_section[NR_SECTION_ROOTS][SECTIONS_PER_ROOT]
	____cacheline_internodealigned_in_smp;
#endif

  从如上代码可看到,section的id分为两部分,一部分是ROOT,另外一部分是PER_ROOT,静态定义数组的方式不仅需要一大块物理连续的内存,同时对于特别稀疏的系统来说也是非常浪费内存的,所以定义CONFIG_SPARSEMEM_EXTREME宏,使用二重指针的是后续的patch更新的方案。二重指针的方式需要动态分配内存,而且SPARSEMEM又必须在系统启动很靠前的地方初始化,有的系统这时还是不支持动态分配内存的,故静态数组的方式还是保留的。

  当enable SPARSEMEM时,pfn_to_page()和page_to_pfn()同样有两种不同的实现方式,一种是经典方式,另一种是vmemmap的方式,它们是通过是否定义CONFIG_SPARSEMEM_VMEMMAP来区分的。
1、经典方式,非vmemmap方式:

#define __page_to_pfn(pg)					\
({	const struct page *__pg = (pg);				\
	int __sec = page_to_section(__pg);			\
	(unsigned long)(__pg - __section_mem_map_addr(__nr_to_section(__sec)));	\
})

#define __pfn_to_page(pfn)				\
({	unsigned long __pfn = (pfn);			\
	struct mem_section *__sec = __pfn_to_section(__pfn);	\
	__section_mem_map_addr(__sec) + __pfn;		\
})

static inline struct page *__section_mem_map_addr(struct mem_section *section)
{
	unsigned long map = section->section_mem_map;
	map &= SECTION_MAP_MASK;
	return (struct page *)map;
}

  从如上代码可知PFN是分为两部分,section id以及相对于section_mem_map的偏移量,PFN转换为struct page时,首先从PFN中截取到section id,然后通过section id在section集合中获得对应的section,再根据section->section_mem_map得到此section对应的struct page数组的起始虚拟地址,最后根据"__section_mem_map_addr(__sec) + __pfn"得到PFN对应struct page地址。

  page转换为PFN时,page对应的section id是保存在page的flags中的,通过section id就获得了对应的section,最后
PFN=page - section->section_mem_map。把section id保存在page的flags的方法的最大问题是page->flags中的bit数目不一定够用,因为这个flags中包含了太多东西,各种page flag,node id,zone id,现在有增加了一个section_nr。更通用的方式就出来了,即vmemmap方式,后面我们会详细介绍。

  PFN既包含了section id,又保存了相对section->section_mem_map的偏移量,那么"page=section_mem_map + pfn"时不就多了section id吗?其实在初始化时,section_mem_map保存的是减去section id的虚拟地址,如下:

static unsigned long sparse_encode_mem_map(struct page *mem_map, unsigned long pnum)
{
	unsigned long coded_mem_map =
		(unsigned long)(mem_map - (section_nr_to_pfn(pnum)));
	...
	return coded_mem_map;
}

static void __meminit sparse_init_one_section(struct mem_section *ms,
		unsigned long pnum, struct page *mem_map,
		unsigned long *pageblock_bitmap)
{
	ms->section_mem_map &= ~SECTION_MAP_MASK;
	ms->section_mem_map |= sparse_encode_mem_map(mem_map, pnum) | SECTION_HAS_MEM_MAP;
 	ms->pageblock_flags = pageblock_bitmap;
}

  section_mem_map指向的此section对应page数组的起始虚拟地址,每个section包含多少page数组成员呢,假设section管理1G大小的内存,物理页大小为4K,那么数组的条目个数为"1G/4K"。SPARSEMEM启动时是一个section,一个section初始化的,为每个section动态分配大小为"sizeof(struct page) * (1G/4K)"byte的内存。配置了SPARSEMEM_EXTREME,SPARSEMEM经典方式的框图如下:
在这里插入图片描述
2、vmemmap方式:
  为了实现SPARSEMEM的vmemmap,在内核的空间布局中,专门增加了一个vmemmap区域,也就是说专门划分出一段虚拟地址空间,用于实现PFN和page的相互转换,SPARSEMEM vmemmap的背后思想是将整个memory map数组映射到一个虚拟地址固定的、连续的vmemmap区域,通过操作vmemmap区域对应的页表,将此区域内需要用到的虚拟地址转换为物理地址,也就是说只会操作active的区域,当然对于一个具体的物理内存页来说,其对应的struct page的虚拟地址是固定的。

  SPARSEMEM vmemmap为了能把所有物理页和struct page映射起来需要很大一段虚拟地址空间,所以这种方式在32位的系统中不太适合,因为一般来说,32位系统中实际的物理内存大小是接近或者超过虚拟地址空间的,虚拟空间本身就捉襟见肘了。对于64位的系统,虚拟空间很大,使用SPARSEMEM vmemmap方式就很适合了。

  既要在内存布局中增加vmemmap区域,又要额外操作vmemmap区域对应的页表,那SPARSEMEM vmemmap有什么好处了?首先它使PFN和page之间相互转换的方式变得很简单;再一个对于一些能支持TLBs映射内核空间的体系结构来说,使用SPARSEMEM vmemmap方式,PFN和page之间的相互转换相对于CONFIG_FLATMEM可能会更高效,因为FLATMEM需要先读取变量"mem_map"保存的值,再加减offset才能得到相应的page或者PFN,但vmemmap就是一个常数,直接加减offset即可。

#define __pfn_to_page(pfn)	(vmemmap + (pfn))
#define __page_to_pfn(page)	(unsigned long)((page) - vmemmap)

  看如上的代码确实简单,且如丝般顺滑。下面是SPARSEMEM vmemmap的是总体框图:
在这里插入图片描述
  我们再来看看SPARSEMEM vmemmap是如何把vmemmap区域的虚拟地址空间和存放struct page数组的物理地址对应起来的。
a、sparse_init_nid()函数会调用sparse_mem_map_populate(),此函数会获取PFN在vmemmap中的虚拟地址,然后操作页
  表,使虚拟地址和物理地址对应起来;
b、sparse_mem_map_populate()首先根据PFN获取到vmemmap区域的虚拟地址,即__pfn_to_page(pfn) (vmemmap + (pfn))
  中的"vmemmap"是一个常量;
c、根据pfn得到虚拟地址后,通过vmemmap_populate()分配实际存放struct page数组的物理内存,同时修改虚拟地址对应的页
  表项,把分配的物理和虚拟地址对应起来;
d、在sparse_init_one_section()的作用是把pfn对应的虚拟地址保存到section_mem_map中;
e、因为section_mem_map保存的是section保存struct page数组的起始虚拟地址,但pfn中又包含了section id,所以
  在sparse_encode_mem_map()中会减去section id的部分。

static void __init sparse_init_nid(int nid, unsigned long pnum_begin,
				   unsigned long pnum_end,
				   unsigned long map_count)
{
	...
	sparse_buffer_init(map_count * section_map_size(), nid);
	for_each_present_section_nr(pnum_begin, pnum) {
		...
		map = sparse_mem_map_populate(pnum, nid, NULL);
		...
		sparse_init_one_section(__nr_to_section(pnum), pnum, map, usemap);
		usemap += usemap_longs;
	}
	...
}

#define __pfn_to_page(pfn)	(vmemmap + (pfn))

struct page * __meminit sparse_mem_map_populate(unsigned long pnum, int nid,
		struct vmem_altmap *altmap)
{
	...
	map = pfn_to_page(pnum * PAGES_PER_SECTION);
	start = (unsigned long)map;
	end = (unsigned long)(map + PAGES_PER_SECTION);

	if (vmemmap_populate(start, end, nid, altmap))
		return NULL;
	return map;
}

static void __meminit sparse_init_one_section(struct mem_section *ms,
		unsigned long pnum, struct page *mem_map,
		unsigned long *pageblock_bitmap)
{
	ms->section_mem_map &= ~SECTION_MAP_MASK;
	ms->section_mem_map |= sparse_encode_mem_map(mem_map, pnum) |
							SECTION_HAS_MEM_MAP;
 	ms->pageblock_flags = pageblock_bitmap;
}

static unsigned long sparse_encode_mem_map(struct page *mem_map, unsigned long pnum)
{
	unsigned long coded_mem_map =
		(unsigned long)(mem_map - (section_nr_to_pfn(pnum)));
	BUILD_BUG_ON(SECTION_MAP_LAST_BIT > (1UL<<PFN_SECTION_SHIFT));
	BUG_ON(coded_mem_map & ~SECTION_MAP_MASK);
	return coded_mem_map;
}

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

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

相关文章

Python 实现Word (DOC或DOCX)与TXT文本格式互转

目录 引言 安装Python库 使用Python将Word转换为TXT文本格式 使用Python将TXT文本格式转换为Word 引言 Word文档和TXT文本文件是日常工作和生活中两种常见的文件格式&#xff0c;各有其特点和优势。Word文档能够保留丰富的格式设置&#xff0c;如字体、段落、表格、图片等…

Flink 生态对 Confluent / Kafka Schema Registry 支持情况的研究报告

文章目录 1. Flink CDC 对 Confluent Schema Registry 的支持情况2. Confluent Avro Format 对 Confluent Schema Registry 的支持情况3. 关键性结论 这几年&#xff0c;在流式链路上引入一个 Schema Registry 变得越来越流行&#xff0c;也越来越有必要&#xff0c; Schema Re…

HX6203是一个完整的电池充电器控制器的两个(8.4V)电池锂离子电池芯片IC

一般描述 该HX6203是一个完整的电池充电器控制器的两个(8.4V)电池锂离子电池。HX6203为快速充电锂离子电池提供了一种小巧、简单、高效的解决方案。一个外部检测电阻以高精度设置充电电流。 内部电阻分压器和精密参考设置的最终浮动电压为8.4V时&#xff0c;输入…

宁盾与深信服发布联合方案,解决云桌面及微软AD完整替代

自 Citrix 退出中国市场后&#xff0c;不少中大型企业关心国产云桌面脱离微软 AD 域是否还能正常工作。在2024年3月初&#xff0c;宁盾身份目录与深信服桌面云完成兼容互认证&#xff0c;对于企业的疑问给出了官方回应。 5月10日&#xff0c;在深信服《Citrix离场背景下&#…

草图大师2024怎么保存低版本呢?插件怎么写?

草图大师是一款流行的绘图和设计软件&#xff0c;为了向后兼容&#xff0c;保存低版本文件时&#xff0c;可以采取以下步骤&#xff1a; su模型库 1.另存为旧版本格式&#xff1a; 在保存文件时&#xff0c;草图大师通常会提供一个选项&#xff0c;让你选择要保存的文件格式和…

plsql 学习

过程化编程语言 赋值&#xff1a;&#xff1a; ||&#xff1a;连接符号 dbms_output.put_line() :输出的语句 var_name ACCOUNTLIBRARY.USERNAME%type; 变量名&#xff1b;某个表的数据类型&#xff1b;赋值给变量名 用下面的方法更好用 异常exception 循…

Mixiy(米思齐)安装

Mixiy(米思齐)安装 官网地址&#xff1a;爱上米思齐 打开官网&#xff0c;选择下图的软件进行下载 复制提取码&#xff0c;点击链接跳转到网盘进行下载&#xff0c;选择(RC4完整版) 下载完成后&#xff0c;解压到合适的位置&#xff0c;进入文件夹&#xff0c;双击Mixly.exe即…

小程序的深层了解

一:wxss的全局样式和局部样式 写在文件上,第一个路径会执行全局和局部自带的wxss给wxml,会执行wxml,会执行json和js. 无论那个文件都会执行文件夹内的和外部的app.wxss,但是如果有一样的属性,则看属性的权重,权重一样,则设置局部样式. 二:全局配置 wx:key"写的是data内…

实操专区-第13周-课堂练习专区-折线图和阶梯图

实操专区-第13周-课堂练习专区-折线图和阶梯图 下载安装ECharts&#xff0c;完成如下样式图形。 代码和截图上传 基本要求&#xff1a;下图2选1&#xff0c;完成代码和截图 完成 3.1.3.12 折线图和阶梯图中的任务点 基本要求&#xff1a;2个选一个完成&#xff0c;多做1个加2分…

Linux网络——TCP协议详细讲解

目录 前言 一、TCP报头格式 1.首部长度 2.窗口大小 3.序号与确认序号 4.标志位 4.1 PSH 4.2 RST 5.紧急指针 6.TCP检验和 二、超时重传 三、连接管理机制 四、滑动窗口 五、拥塞控制 六、延迟应答 七、为什么TCP这么复杂? 前言 前面我们学习了TCP协议套接字的…

DFS:解决二叉树问题

文章目录 了解DFS1.计算布尔二叉树的值思路代码展示 2.求根节点到叶节点数字之和思路代码展示 3.二叉树剪枝思路代码展示 4.验证二叉搜索树思路分析代码展示 5.二叉搜索树中第k小元素思路&#xff1a;代码展示 6.二叉树的所有路径思路分析代码展示 总结 了解DFS 所谓DFS就是就…

编译qt5.15.2(mac/windows)的mysql驱动(附带编译好的文件)

文章目录 0 背景1 编译过程2 福利 0 背景 因为需要连接到mysql数据库&#xff0c;所以需要连mysql驱动。 1 编译过程 1&#xff0c;打开文件/Users/mac/Qt5.14.2/5.14.2/Src/qtbase/src/plugins/sqldrivers/sqldrivers.pro&#xff0c;注释掉QMAKE_USE mysql&#xff1b; 如…

宝藏网站推荐-封面图片生成器

封面图片生成器&#xff1a;封面图生成器 | 太空编程 (spacexcode.com)[https://spacexcode.com/coverview] 由来 最近爱上了写文案&#xff0c;在网上冲浪的时候发现一个宝藏网站。Spacecode&#xff0c;一个大神维护的个人网站&#xff0c;含有前端知识库、个人博客及他做…

【设计模式】JAVA Design Patterns——Commander(指挥官模式)

&#x1f50d;目的 用于处理执行分布式事务时可能遇到的所有问题。 &#x1f50d;解释 处理分布式事务很棘手&#xff0c;但如果我们不仔细处理&#xff0c;可能会带来不想要的后果。假设我们有一个电子商务网站&#xff0c;它有一个支付微服务和一个运输微服务。如果当前运输…

基于jeecgboot-vue3的Flowable增加表单功能(三)

因为这个项目license问题无法开源&#xff0c;更多技术支持与服务请加入我的知识星球。 接上一节 10、新建表单&#xff0c;打开VForm3编辑页面 /*** 新增事件*/function handleAdd() {designer.open true;nextTick(() > {reset();vfDesignerRef.value.clearDesigner();}…

R实验 基础(四)

实验目的&#xff1a; 掌握R对数据文件的读、写操作&#xff1b;了解R基础包中的数据集及相关的操作。 实验内容&#xff1a; 读取纯文本文件。在R语言基础包中&#xff0c;有两个函数可以使用&#xff1a; read.table()函数&#xff08;包括read.csv()函数&#xff09; 只…

【html】网页布局模板01---简谱风

模板效果: 这是一种最简单,最干净的一种网页布局。 模板介绍: 模板概述: 这个模板是一个基础的网页布局模板,包括一个头部区域(header),其中包含网站标题(logo)和导航菜单(nav),以及一个页脚区域(copy),用于显示版权信息。整体布局简洁明了,适合作为各种类…

TypeScript-搭建编译环境

搭建编译环境 TypeScript 编写的代码是无法直接在js引擎( 浏览器 / Nodejs )中运行的&#xff0c;最终还需要经过编译成js代码才可以正常运行 搭建手动编译环境 1️⃣ 全局安装 typescript 包&#xff08;编译引擎&#xff09; -> 注册 tsc 命令 npm i -g typescript 2…

Julia编程01:Julia语言介绍

在2020上半年&#xff0c;因为疫情无法返校&#xff0c;所以在家待了半年&#xff0c;期间学习一点了R语言、Python、Julia、linux和C语言&#xff0c;只是学习基础语法并没有项目练习&#xff0c;因此返校半年后差不多都不记得了&#xff0c;现在重新捡起Julia丰富下当时写的笔…

MyBatis实用方案,如何使项目兼容多种数据库

系列文章目录 MyBatis缓存原理 Mybatis plugin 的使用及原理 MyBatisSpringboot 启动到SQL执行全流程 数据库操作不再困难&#xff0c;MyBatis动态Sql标签解析 Mybatis的CachingExecutor与二级缓存 使用MybatisPlus还是MyBaits &#xff0c;开发者应该如何选择&#xff1f; 巧…