Linux物理内存管理——会议室管理员如何分配会议室

news2024/11/27 0:16:07

之前学习了站在内存的角度去看内存,看到的都是虚拟内存,这些虚拟内存总是要映射到物理页面的,这一篇文章来学习物理内存是如何管理的。

物理内存的组织方式

之前学习虚拟内存的时候,当涉及物理内存的映射的时候,总是把内存想象成它是由连续的一页一页的块组成的。我们可以从0开始对物理页编号,这样每个物理页都会有个号。

整个物理内存的布局就非常简单、易管理,这就是最经典的平坦内存模型(Flat Memory Model): 由于物理地址是连续的,页也是连续的,每个页大小也是一样的。因而对于任何一个地址,只要直接除一下每页的大小,很容易直接算出在哪一页。每个页有一个结构struct page表示,这个结构也是放在一个数组里面,这样根据页号,很容易通过下标找到相应的struct page结构。

在X86的工作模式下,CPU是通过总线去访问内存的,这就是最经典的内存使用方式。在这种模式下,CPU也会有多个,在总线的一侧。所有的内存条组成一大片内存,在总线的另一侧,所有的CPU访问内存都要过总线,而且距离都是一样的,这种模式称为SMP(Symmetric multiprocessing),即对称多处理器。当然,它也有一个显著的缺点,就是总线会成为瓶颈,因为数据都要走它

 为了提高性能和可扩展性,后来有了一种更高级的模式,NUMA(Non-uniform memory access),非一致内存访问。在这种模式下,内存不是一整块每个CPU都有自己的本地内存CPU访问本地内存不用过总线,因而速度要快很多,每个CPU和内存在一起,称为一个NUMA节点。但是,在本地内存不足的情况下,每个CPU都可以去另外的NUMA节点申请内存,这个时候访问延时就会比较长。这样,内存被分成了多个节点,每个节点再被分成一个一个的页面。由于页需要全局唯一定位,页还是需要有全局唯一的页号的。但是由于物理内存不是连起来的了,页号也就不再连续了。于是内存模型就变成了非连续内存模型,管理起来就复杂一些。这里需要指出的是,NUMA往往是非连续内存模型。而非连续内存模型不一定就是NUMA,有时候一大片内存的情况下,也会有物理内存地址不连续的情况。

 节点

我们主要解析的是NUMA方式,这也是当前主流的场景。

首先要能够表示NUMA节点的概念,于是有了下面这个结构typedef struct pglist_data pg_data_t,它里面有以下的成员变量:

  • 每一个节点都有自己的ID:node_id;

  • node_mem_map就是这个节点的struct page数组,用于描述这个节点里面的所有的页

  • node_start_pfn是这个节点的起始页号

  • node_spanned_pages是这个节点中包含不连续的物理内存地址的页面数

  • node_present_pages是真正可用的物理页面的数目

 例如,64M物理内存隔着一个4M的空洞,然后是另外的64M物理内存。这样换算成页面数目就是,16K个页面隔着1K个页面,然后是另外16K个页面。这种情况下,node_spanned_pages就是33K个页面,node_present_pages就是32K个页面。

typedef struct pglist_data {
	struct zone node_zones[MAX_NR_ZONES];
	struct zonelist node_zonelists[MAX_ZONELISTS];
	int nr_zones;
	struct page *node_mem_map;
	unsigned long node_start_pfn;
	unsigned long node_present_pages; /* total number of physical pages */
	unsigned long node_spanned_pages; /* total size of physical page range, including holes */
	int node_id;
......
} pg_data_t;

每一个节点分成一个个区域zone,放在数组node_zones里面。这个数组的大小为MAX_NR_ZONES。区域的定义如下:

enum zone_type {
#ifdef CONFIG_ZONE_DMA
	ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
	ZONE_DMA32,
#endif
	ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
	ZONE_HIGHMEM,
#endif
	ZONE_MOVABLE,
	__MAX_NR_ZONES
};

ZONE_DMA是指可用于作DMA(Direct Memory Access,直接内存存取的内存。DMA是这样一种机制:要把外设的数据读入内存或把内存的数据传送到外设,原来都要通过CPU控制完成,但是这会占用CPU,影响CPU处理其他事情,所以有了DMA模式。CPU只需向DMA控制器下达指令让DMA控制器来处理数据的传送数据传送完毕再把信息反馈给CPU,这样就可以解放CPU

对于64位系统,有两个DMA区域。除了上面说的ZONE_DMA,还有ZONE_DMA32。在这里你大概理解DMA的原理就可以,不必纠结,我们后面会讲DMA的机制。

ZONE_NORMAL是直接映射区,就是上一节讲的,从物理内存到虚拟内存的内核区域,通过加上一个常量直接映射。

ZONE_HIGHMEM是高端内存区,就是上一节讲的,对于32位系统来说超过896M的地方,对于64位没必要有的一段区域

ZONE_MOVABLE是可移动区域,通过将物理内存划分为可移动分配区域和不可移动分配区域来避免内存碎片

对于区域的划分,都是针对物理内存的。 

nr_zones表示当前节点的区域的数量。node_zonelists是备用节点和它的内存区域的情况。前面讲NUMA的时候,我们讲了CPU访问内存,本节点速度最快,但是如果本节点内存不够怎么办,还是需要去其他节点进行分配。毕竟,就算在备用节点里面选择,慢了点也比没有强。

既然整个内存被分成了多个节点,那pglist_data应该放在一个数组里面。每个节点一项,就像下面代码里面一样:

struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;

区域

 我们把内存分成了节点,把节点分成了区域。接下来看看一个区域内是如何组织的,表示区域的数据结构zone的定义如下:

struct zone {
......
	struct pglist_data	*zone_pgdat;
	struct per_cpu_pageset __percpu *pageset;


	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;
	 *
	 */
	unsigned long		managed_pages;
	unsigned long		spanned_pages;
	unsigned long		present_pages;


	const char		*name;
......
	/* 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;
......
} ____cacheline_internodealigned_in_

在一个zone里面,zone_start_pfn表示属于这个zone的第一个页

如果我们仔细看代码的注释,可以看到,spanned_pages = zone_end_pfn - zone_start_pfn,也即spanned_pages指的是不管中间有没有物理内存空洞,反正就是最后的页号减去起始的页号

present_pages = spanned_pages - absent_pages(pages in holes),也即present_pages是这个zone在物理内存中真实存在的所有page数目

managed_pages = present_pages - reserved_pages,也即managed_pages是这个zone被伙伴系统管理的所有的page数目,伙伴系统的工作机制我们后面会讲。

per_cpu_pageset用于区分冷热页。什么叫冷热页呢?为了让CPU快速访问段描述符,在CPU里面有段描述符缓存。CPU访问这个缓存的速度比内存快得多。同样对于页面来讲,也是这样的。如果一个页被加载到CPU高速缓存里面这就是一个热页(Hot Page),CPU读起来速度会快很多,如果没有就是冷页(Cold Page)。由于每个CPU都有自己的高速缓存,因而per_cpu_pageset也是每个CPU一个。

组成物理内存的基本单位,页的数据结构struct page,里面有很多的union,union结构是在C语言中被用于同一块内存根据情况保存不同类型数据的一种方式。这里之所以用了union,是因为一个物理页面使用模式有多种。

第一种模式,要用就用一整页。这一整页的内存,或者直接和虚拟地址空间建立映射关系,我们把这种称为匿名页(Anonymous Page)。或者用于关联一个文件,然后再和虚拟地址空间建立映射关系,这样的文件,我们称为内存映射文件(Memory-mapped File)。

如果某一页是这种使用模式,则会使用union中的以下变量:

  • struct address_space *mapping就是用于内存映射,如果是匿名页,最低位为1;如果是映射文件,最低位为0;

  • pgoff_t index是在映射区的偏移量

  • atomic_t _mapcount,每个进程都有自己的页表,这里指有多少个页表项指向了这个页

  • struct list_head lru表示这一页应该在一个链表上,例如这个页面被换出,就在换出页的链表中

  • compound相关的变量用于复合页(Compound Page),就是将物理上连续的两个或多个页看成一个独立的大页

第二种模式,仅需分配小块内存。有时候,我们不需要一下子分配这么多的内存,例如分配一个task_struct结构,只需要分配小块的内存,去存储这个进程描述结构的对象。为了满足对这种小内存块的需要,Linux系统采用了一种被称为slab allocator的技术,用于分配称为slab的一小块内存。它的基本原理是从内存管理模块申请一整块页,然后划分成多个小块的存储池,用复杂的队列来维护这些小块的状态(状态包括:被分配了/被放回池子/应该被回收)。

也正是因为slab allocator对于队列的维护过于复杂,后来就有了一种不使用队列的分配器slub allocator,后面我们会解析这个分配器。但是你会发现,它里面还是用了很多slab的字眼,因为它保留了slab的用户接口,可以看成slab allocator的另一种实现。

还有一种小块内存的分配器称为slob,非常简单,主要使用在小型的嵌入式系统。

如果某一页是用于分割成一小块一小块的内存进行分配的使用模式,则会使用union中的以下变量:

  • s_mem是已经分配了正在使用的slab的第一个对象;

  • freelist是池子中的空闲对象;

  • rcu_head是需要释放的列表。

    struct page {
    	unsigned long flags;
    	union {
    		struct address_space *mapping;	
    		void *s_mem;			/* slab first object */
    		atomic_t compound_mapcount;	/* first tail page */
    	};
    	union {
    		pgoff_t index;		/* Our offset within mapping. */
    		void *freelist;		/* sl[aou]b first free object */
    	};
    	union {
    		unsigned counters;
    		struct {
    			union {
    				atomic_t _mapcount;
    				unsigned int active;		/* SLAB */
    				struct {			/* SLUB */
    					unsigned inuse:16;
    					unsigned objects:15;
    					unsigned frozen:1;
    				};
    				int units;			/* SLOB */
    			};
    			atomic_t _refcount;
    		};
    	};
    	union {
    		struct list_head lru;	/* Pageout list	 */
    		struct dev_pagemap *pgmap; 
    		struct {		/* slub per cpu partial pages */
    			struct page *next;	/* Next partial slab */
    			int pages;	/* Nr of partial slabs left */
    			int pobjects;	/* Approximate # of objects */
    		};
    		struct rcu_head rcu_head;
    		struct {
    			unsigned long compound_head; /* If bit zero is set */
    			unsigned int compound_dtor;
    			unsigned int compound_order;
    		};
    	};
    	union {
    		unsigned long private;
    		struct kmem_cache *slab_cache;	/* SL[AU]B: Pointer to slab */
    	};
    ......
    }

 页的分配

前面已经讲了物理内存的组织,从节点到区域到页到小块。接下来来看物理内存的分配

对于要分配比较大的内存,例如到分配页级别的,可以使用伙伴系统(Buddy System)。

Linux中的内存管理的“页”大小为4KB。把所有的空闲页分组为11个页块链表,每个块链表分别包含很多个大小的页块,有1、2、4、8、16、32、64、128、256、512和1024个连续页的页块。最大可以申请1024个连续页,对应4MB大小的连续内存。每个页块的第一个页的物理地址是该页块大小的整数倍。

 第i个页块链表中,页块中页的数目为2^i。

在struct zone里面有以下的定义:

struct free_area	free_area[MAX_ORDER];

MAX_ORDER就是指数。

#define MAX_ORDER 11

当向内核请求分配(2^(i-1),2^i]数目的页块时,按照2^i页块请求处理。如果对应的页块链表中没有空闲页块,那我们就在更大的页块链表中去找。当分配的页块中有多余的页时,伙伴系统会根据多余的页块大小插入到对应的空闲页块链表中。例如,要请求一个128个页的页块时,先检查128个页的页块链表是否有空闲块。如果没有,则查256个页的页块链表;如果有空闲块的话,则将256个页的页块分成两份,一份使用,一份插入128个页的页块链表中。如果还是没有,就查512个页的页块链表;如果有的话,就分裂为128、128、256三个页块,一个128的使用,剩余两个插入对应页块链表。

static inline struct page *
alloc_pages(gfp_t gfp_mask, unsigned int order)
{
	return alloc_pages_current(gfp_mask, order);
}


/**
 * 	alloc_pages_current - Allocate pages.
 *
 *	@gfp:
 *		%GFP_USER   user allocation,
 *      	%GFP_KERNEL kernel allocation,
 *      	%GFP_HIGHMEM highmem allocation,
 *      	%GFP_FS     don't call back into a file system.
 *      	%GFP_ATOMIC don't sleep.
 *	@order: Power of two of allocation size in pages. 0 is a single page.
 *
 *	Allocate a page from the kernel page pool.  When not in
 *	interrupt context and apply the current process NUMA policy.
 *	Returns NULL when no page can be allocated.
 */
struct page *alloc_pages_current(gfp_t gfp, unsigned order)
{
	struct mempolicy *pol = &default_policy;
	struct page *page;
......
	page = __alloc_pages_nodemask(gfp, order,
				policy_node(gfp, pol, numa_node_id()),
				policy_nodemask(gfp, pol));
......
	return page;
}

alloc_pages会调用alloc_pages_current,这里面的注释比较容易看懂了,gfp表示希望在哪个区域中分配这个内存:

  • GFP_USER用于分配一个页映射到用户进程的虚拟地址空间,并且希望直接被内核或者硬件访问,主要用于一个用户进程希望通过内存映射的方式,访问某些硬件的缓存,例如显卡缓存;

  • GFP_KERNEL用于内核中分配页,主要分配ZONE_NORMAL区域,也即直接映射区;

  • GFP_HIGHMEM,顾名思义就是主要分配高端区域的内存。

另一个参数order,就是表示分配2的order次方个页。

接下来调用__alloc_pages_nodemask。这是伙伴系统的核心方法。它会调用get_page_from_freelist。这里面的逻辑也很容易理解,就是在一个循环中先看当前节点的zone。如果找不到空闲页,则再看备用节点的zone。

static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
						const struct alloc_context *ac)
{
......
	for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx, ac->nodemask) {
		struct page *page;
......
		page = rmqueue(ac->preferred_zoneref->zone, zone, order,
				gfp_mask, alloc_flags, ac->migratetype);
......
}

每一个zone,都有伙伴系统维护的各种大小的队列,就像上面伙伴系统原理里讲的那样。这里调用rmqueue就很好理解了,就是找到合适大小的那个队列,把页面取下来。

接下来的调用链是rmqueue->__rmqueue->__rmqueue_smallest。在这里,我们能清楚看到伙伴系统的逻辑。

static inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
						int migratetype)
{
	unsigned int current_order;
	struct free_area *area;
	struct page *page;


	/* Find a page of the appropriate size in the preferred list */
	for (current_order = order; current_order < MAX_ORDER; ++current_order) {
		area = &(zone->free_area[current_order]);
		page = list_first_entry_or_null(&area->free_list[migratetype],
							struct page, lru);
		if (!page)
			continue;
		list_del(&page->lru);
		rmv_page_order(page);
		area->nr_free--;
		expand(zone, page, order, current_order, area, migratetype);
		set_pcppage_migratetype(page, migratetype);
		return page;
	}


	return NULL;

总结:

 

这一篇文章我们主要讲了物理内存的组织形式,就像下面图中展示的一样。

如果有多个CPU,那就有多个节点。每个节点用struct pglist_data表示,放在一个数组里面。

每个节点分为多个区域,每个区域用struct zone表示,也放在一个数组里面。

每个区域分为多个页。为了方便分配,空闲页放在struct free_area里面,使用伙伴系统进行管理和分配,每一页用struct page表示。

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

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

相关文章

不会还有人不知道如何搭建【关键字驱动自动化测试框架】吧 ?

前言 这篇文章我们将了解关键字驱动测试又是如何驱动自动化测试完成整个测试过程的。关键字驱动框架是一种功能自动化测试框架&#xff0c;它也被称为表格驱动测试或者基于动作字的测试。关键字驱动的框架的基本工作是将测试用例分成四个不同的部分。首先是测试步骤&#xff0…

MobileNet网络模型(V1,V2,V3)

MobileNet网络中的亮点&#xff1a;DW卷积&#xff0c;增加了两个超参数&#xff0c;控制卷积层卷积核个数的α&#xff0c;控制输入图像大小的β&#xff0c;这两个超参数是我们人为设定的&#xff0c;并不是学习到的。BN batch normal批规范化&#xff0c;为了加快训练收敛速…

大数据MapReduce学习案例:数据去重

文章目录一&#xff0c;案例分析&#xff08;一&#xff09;数据去重介绍&#xff08;二&#xff09;案例需求二&#xff0c;案例实施&#xff08;一&#xff09;准备数据文件&#xff08;1&#xff09;启动hadoop服务&#xff08;2&#xff09;在虚拟机上创建文本文件&#xf…

大数据MapReduce学习案例:TopN

文章目录一&#xff0c;案例分析&#xff08;一&#xff09;TopN分析法介绍&#xff08;二&#xff09;案例需求二&#xff0c;案例实施&#xff08;一&#xff09;准备数据文件&#xff08;1&#xff09;启动hadoop服务&#xff08;2&#xff09;在虚拟机上创建文本文件&#…

linux把乱码文件(非文件内容)删除(Xshell中使用rz命令上传文件出现乱码,删除乱码文件)的步骤讲解

我的场景是&#xff1a;首先安装lrzsz&#xff1a;yum install lrzsz&#xff0c;然后后使用rz -be上传文件出现乱码问题&#xff0c;想要把乱码文件删除 圆圈圈出来的就是乱码文件&#xff0c;横线划线出来的是使用rm命令删除但是无效 解决方法是&#xff1a;ls | grep -v ‘…

人人都是数据分析师

一.耳熟能详的数据你真的了解吗&#xff1f; 1.数据的类型 根据数据的存储形式&#xff0c;可以将数据分为结构化数据和非结构化数据两种类型 存储在数据库中的结构化数据能够很方便地进行检索、分析以及展示分析结果。结构化数据是进 行数据分析的基本类型&#xff0c;大多数…

【1697. 检查边长度限制的路径是否存在】

来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 给你一个 n 个点组成的无向图边集 edgeList &#xff0c;其中 edgeList[i] [ui, vi, disi] 表示点 ui 和点 vi 之间有一条长度为 disi 的边。请注意&#xff0c;两个点之间可能有 超过一条边 。 给…

(附源码)springboot建达集团公司平台 毕业设计 141538

springboot建达集团公司平台 摘 要 随着互联网大趋势的到来&#xff0c;社会的方方面面&#xff0c;各行各业都在考虑利用互联网作为媒介将自己的信息更及时有效地推广出去&#xff0c;而其中最好的方式就是建立网络管理系统&#xff0c;并对其进行信息管理。由于现在网络的发…

DAMOYOLO:基于DAMOYOLO训练数据集详细教程

前段时间yolov7的推出引起一篇热潮&#xff0c;接着rmyolo紧跟潮流&#xff0c;后面阿里的达摩院也推出了自己的yolo算法&#xff0c;怎么说呢&#xff0c;damoyolo推出依旧不少天了&#xff0c;现在才写博客&#xff0c;因为damoyolo给我的体验感不是很好。 先看下DAMOYOLO的…

安科瑞模拟信号隔离器BM-DIS/I 经2000V隔离输出DC4-20mA 二线制

安科瑞 王晶淼/刘芳 1.信号隔离器功能 BM系列模拟信号隔离器可以对电流、电压等电量参数或温度、电阻等非电量参数进行高速精确测量&#xff0c;经隔离转换成标准的模拟信号输出。既可直接与指针表、数显表相接&#xff0c;也可以与自控仪表&#xff08;如PLC&#xff09;、各…

nginx负载均衡实战练习

1、简介 nginx是一个web服务器&#xff0c;反向代理服务器、开源并且高性能&#xff0c;社区里面有很多工程师在维护这个项目。可以在官网&#xff08;Index of /download/&#xff09;下载组件。而且nginx可以用来做流量转发&#xff0c;也是是负载均衡功能&#xff0c;分散单…

160. 相交链表

给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交&#xff1a; 题目数据 保证 整个链式结构中不存在环。 注意&#xff0c;函数返回结果后…

富而喜悦一年一渡专属于你的特别的礼物!快来收!

过去的一年&#xff0c;你过得怎么样&#xff1f;是否有过艰难的逆流时刻&#xff0c;是否拥有过快乐和满足&#xff0c;又是否得到了成长和收获&#xff1f;富而喜悦2023一年一渡财富流新年主题活动就要给你一个礼物多多&#xff01;美美的“礼物”活动&#xff01; 为此&…

虹科分享 | 虹科Dimetix激光测距传感器如何利用反射来测量?(上)

-01-测量原理 ● 反射调制激光 采用激光振幅的高频调制&#xff0c;并评估这些调制的高频信号(脉冲串)的相位和距离。激光束在短间隔内被放大调制&#xff0c;这使得它能够非常迅速地测量单个脉冲包的与距离有关的时间偏移&#xff0c;而且还能测量单个波在调制包内相互之间的…

[附源码]Nodejs计算机毕业设计基于Web学术会议投稿管理系统Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…

Ims开机注册流程

目录 概述Ims注册时序图PhoneApp的启动过程Ims注册主要代码总结概述 IMS(IP Multimedia Subsystem)是IP多媒体系统, 是一种新的多媒体业务形式&#xff0c;ims service 结构主要包括 ImsService、ImsManager、MmTelFeatureConnection、ImsCallSession。其中&#xff1a; ImsS…

关于java 操作word的几种方式

1.apose word <dependency><groupId>com.luhuiguo</groupId><artifactId>aspose-words</artifactId><version>22.4</version><type>pom</type> </dependency> 一般用来转换文件格式&#xff0c;对于读取创建段…

ArcGIS:Excel/Txt 文件生成点图层、属性表编辑的基本方法、属性表之间的连接(合并)和关联的操作、属性表的字段计算器的使用

目录 01 说明 02 实验目的及要求 03 实验设备及软件平台 03 实验原理 04 实验内容与步骤 01 说明 由于这次的作业是从word上粘贴过来&#xff0c;所以有一些格式修改不了&#xff0c;也没有时间和精力修改&#xff0c;所以没有详细目录等等&#xff0c;浏览的时候应该非常难受.…

前端基础(六)_CSS单位

CSS单位 px px 相对于显示器屏幕分辨率而言&#xff0c;值固定&#xff0c;计算比较容易 em em 相对长度单位 和父元素的字号大小有关系 font-size属性值而言 浏览器默认字号为16像素&#xff0c;未经调整的浏览器都符合1em16px div p都设置了font-size 那em就是随p特点&am…

2022-12-10 Set类型

set类型 新的存储需求&#xff0c;存储大量的数据&#xff0c;在查询方面提供更高的效率。 需要的存储结构&#xff1a;能够保存大量的数据&#xff0c;高效的内部存储机制&#xff0c;便于查询。 set类型&#xff1a;与hash存储结构完全相同&#xff0c;仅存储键&#xff0c…