从零手写操作系统之RVOS内存管理模块简单实现-02

news2024/11/28 14:09:59

从零手写操作系统之RVOS内存管理模块简单实现-02

  • 内存管理分类
  • 内存映射表(Memory Map)
  • Linker Script 链接脚本
    • 语法
    • 基于符号定义获取程序运行时内存分布
  • 基于 Page 实现动态内存分配
    • 代码讲解
    • 调试
  • 扩展


本系列参考: 学习开发一个RISC-V上的操作系统 - 汪辰 - 2021春 整理而来,主要作为xv6操作系统学习的一个前置基础。

RVOS是本课程基于RISC-V搭建的简易操作系统名称。

课程代码和环境搭建教程参考github仓库: https://github.com/plctlab/riscv-operating-system-mooc/blob/main/howto-run-with-ubuntu1804_zh.md

前置知识:

  • RVOS环境搭建-01

内存管理分类

  • 自动管理内存 - 栈 (Stack)
  • 静态内存 - 全局变量/静态变量
  • 动态管理内存 - 堆(heap)

内存映射表(Memory Map)

在这里插入图片描述
可执行文件中各个段在虚拟内存中的地址,在链接阶段确定,然后程序装载阶段,就按照各个段在链接阶段设置好的虚拟地址进行装载。

此部分内容详细可参考<<程序员的自我修养—装载,链接和库>>一书


Linker Script 链接脚本

链接器一般都提供多种控制整个链接过程的方法,以用来产生用户所须要的文件。

一般链接器有如下三种方法:

  • 使用命令行来给链接器指定参数,ld的-o、-e参数就属于这类。
  • 将链接指令存放在目标文件里面,编译器经常会通过这种方法向链接器传递指令。方法也比较常见,只是我们平时很少关注,比如VISUAL C++编译器会把链接参数放在PE目标文件的.drectve段以用来传递参数。
  • 使用链接控制脚本,使用链接控制脚本方法就是本节要介绍的,也是最为灵活、最为强大的链接控制方法。

由于各个链接器平台的链接控制过程各不相同,我们只能侧重一个平台来介绍。ld链接器的链接脚本功能非常强大,我们接下来以ld作为主要介绍对象。

ld 在用户没有指定链接脚本的时候会使用默认链接脚本。我们可以使用下面的命令行来查看ld默认的链接脚本:

ld -verbose

默认的ld链接脚本存放在/usr/lib/ldscripts/下,不同的机器平台、输出文件格式都有相应的链接脚本。

  • 比如Intel IA32下的普通可执行ELF文件链接脚本文件为elf_i386.x;
  • IA32下共享库的链接脚本文件为elf_i386.xs等。

ld会根据命令行要求使用相应的链接脚本文件来控制链接过程,当我们使用ld来链接生成一个可执行文件的时候,它就会使用elf_i386.x作为链接控制脚本;

当我们使用ld来生成一个共享目标文件的时候,它就会使用elf_i386.xs作为链接控制脚本。

当然,为了更加精确地控制链接过程,我们可以自己写一个脚本,然后指定该脚本为链接控制脚本。比如可以使用-T参数:

ld –T link.script

什么情况下需要使用链接脚本?

绝大部分情况下,我们使用链接器提供的默认链接规则对目标文件进行链接。这在一般情况下是没有问题的,但对于一些特殊要求的程序,比如:

  • 操作系统内核、BIOS(Basic Input Output System)或一些在没有操作系统的情况下运行的程序(如引导程序Boot Loader或者嵌入式系统的程序,或者有一些脱离操作系统的硬盘分区软件PQMagic等),以及另外的一些须要特殊的链接过程的程序,如一些内核驱动程序等,它们往往受限于一些特殊的条件,如须要指定输出文件的各个段虚拟地址、段的名称、段存放的顺序等,因为这些特殊的环境,特别是某些硬件条件的限制,往往对程序的各个段的地址有着特殊的要求。

在编译普通的应用程序时,可以使用默认的链接器脚本,但是对于内核程序来说,它本身也是一个.elf文件,这个.elf文件该怎么组织,各个段放到内存中什么地方,这个由于和底层硬件强相关,所以需要我们自己编写相关的链接器脚本:

  • 在之前的环境准备小节中,我们makefile文件中编写的ld链接命令中只通过-Ttext=0x80000000命令指明了代码段的在内存中的起始地址
os.elf: ${OBJS}
	${CC} ${CFLAGS} -Ttext=0x80000000 -o os.elf $^
	${OBJCOPY} -O binary os.elf os.bin
  • 但是在本节中我们将会使用链接器脚本文件os.ld来描述整个链接过程
    在这里插入图片描述

语法

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

.代表当前所处的内存地址

在这里插入图片描述

链接器会把定义符号放入符号表中,符号表中的符号是我们可以在程序中访问到的。

链接器语法详细内容可以参考GUN文档,或者程序员自我修养–装载,链接与库的4.5节。


基于符号定义获取程序运行时内存分布

在这里插入图片描述

参考课程02节的os.ld链接器脚本文件

如何在代码中获取在链接器脚本中定义的相关符号值呢?

在这里插入图片描述

参考课程02节mem.s文件

注意:

  • 在C代码中直接获取链接器脚本中定义的符号是有一定的限制的。C语言是一种静态编译语言,在编译时会将源代码转换为机器码,并生成可执行文件。链接器脚本用于指导链接器如何组织可执行文件的各个部分,包括代码段、数据段、符号表等。
  • 在C代码中,无法直接引用链接器脚本中定义的符号的值,因为C编译器并不了解链接器脚本的细节。C编译器只能根据给定的C代码进行编译,将代码转换为机器码,并生成符号表。符号表中包含了在C代码中定义的全局变量、函数等符号及其对应的地址。
  • 要在C代码中获取链接器脚本中定义的符号的值,一种常见的做法是通过在C代码中声明外部变量,并使用链接器脚本中定义的符号来初始化这些外部变量。这样,链接器在链接阶段会将外部变量与链接器脚本中定义的符号关联起来,并将符号的值赋给外部变量。然后,C代码就可以通过访问这些外部变量来获取链接器脚本中定义的符号的值。
  • 总之,C代码无法直接获取链接器脚本中定义的符号的值,但可以通过声明外部变量并与符号关联来间接获取。这种间接的方式使得C代码能够与链接器脚本进行交互,并共享符号的值。

在c程序中获取链接器脚本中定义的符号,有两种方式:

  • 链接器脚本中使用PROVIDER定义符号,并在c语言中通过extern声明外部变量进行绑定
SECTIONS
{
  .text :
  {
    *(.text)
  }
  
  .data :
  {
    *(.data)
  }
  
  .bss :
  {
    *(.bss)
  }
  
  /* 定义一个名为 _custom_symbol 的符号,并将其赋值为 42 */
  PROVIDE(_custom_symbol = 42);
}


#include <stdio.h>
extern int _custom_symbol;
int main() {
    printf("The value of _custom_symbol is: %d\n", _custom_symbol);
    return 0;
}
  • 通过汇编定义一个全局变量绑定到链接器脚本中的符号,c程序中定义extern变量和汇编文件中定义的全局变量相绑定
SECTIONS
{
  /* ...其他部分... */

  /* 定义一个名为 _asm_var 的符号,并将其赋值为 100 */
  PROVIDE(_asm_var = 100);
}

.section .data
.global asm_var
asm_var:
  .word _asm_var


#include <stdio.h>
extern int asm_var;
int main() {
    printf("The value of asm_var is: %d\n", asm_var);
    return 0;
}

将汇编文件作为绑定的中间转换层有以下几个好处:

  1. 灵活性:使用汇编文件可以更加灵活地控制符号的定义和绑定。你可以直接在汇编文件中定义符号,并将其与链接器脚本中的符号绑定,而不依赖于C语言的语法和限制。这使得你可以更精确地控制符号的位置、大小和属性。

  2. 细粒度控制:汇编语言提供了更细粒度的控制能力。你可以直接使用汇编指令来定义变量、设置符号的初始值,以及指定变量的大小和对齐方式。这使得你可以更好地适应特定的需求,如嵌入式系统的内存布局和对齐要求。

  3. 可读性:使用汇编文件作为绑定的中间转换层可以提高代码的可读性和可维护性。通过将符号的定义和绑定从链接器脚本和C代码中分离出来,可以更清晰地表达代码的意图,并使得代码更易于理解和修改。

  4. 跨平台支持:使用汇编文件作为中间转换层可以更好地支持跨平台开发。汇编语言是与硬件平台相关的,通过直接编写汇编代码,可以更好地适应不同的硬件架构和操作系统环境。这使得你的代码更具可移植性和可扩展性。

总之,通过将汇编文件作为绑定的中间转换层,可以提供更大的灵活性、细粒度的控制能力,提高代码的可读性和可维护性,以及更好地支持跨平台开发。这对于一些特定的需求和项目来说是非常有益的。


基于 Page 实现动态内存分配

在这里插入图片描述
数据结构设计:
在这里插入图片描述
此处采用数组方式来管理内存。


代码讲解

此部分代码基于课程02小节的page.c文件展开讲解

  • 获取链接器脚本中定义的符号,这些变量在链接器链接过程中计算得出
/*
 * Following global vars are defined in mem.S
 */
extern uint32_t TEXT_START;
extern uint32_t TEXT_END;
extern uint32_t DATA_START;
extern uint32_t DATA_END;
extern uint32_t RODATA_START;
extern uint32_t RODATA_END;
extern uint32_t BSS_START;
extern uint32_t BSS_END;
extern uint32_t HEAP_START;
extern uint32_t HEAP_SIZE;
  • 堆区的范围和最大能够分配的页数量
/*
 * _alloc_start points to the actual start address of heap pool
 * _alloc_end points to the actual end address of heap pool
 * _num_pages holds the actual max number of pages we can allocate.
 */
static uint32_t _alloc_start = 0;
static uint32_t _alloc_end = 0;
static uint32_t _num_pages = 0;

对于数据结构的选择,我们这里选取数组结构:
在这里插入图片描述
由于物理内存被划分为一块块固定大小的内存,所以我们可以通过附加索引信息记录某个页是否已经分配出去,并且索引记录的下标和对应的物理页下标进行映射,映射公式为:

  • 物理页地址=alloc_start + 索引下标 * PAGE_SIZE

并且我们使用Page结构体来作为索引记录,用于表示某个物理页是否已经分配出去,并且由于用户通常一次性申请好几个连续物理页,释放的时候传入分配内存起始地址,我们需要回收先前分配给该用户的多个连续物理页,因此还需要一个记号标记当前物理页是否为某次连续分配中的最后一个物理页:

/*
 * Page Descriptor 
 * flags:
 * - bit 0: flag if this page is taken(allocated)
 * - bit 1: flag if this page is the last page of the memory block allocated
 */
struct Page {
	uint8_t flags;
};
  • 利用flags标记的第0位表示物理页是否分配
  • 利用flags标记的第1位表示是否为某次分配中的最后一个物理页

内存管理模块初始化:

void page_init()
{
	/* 
	 * We reserved 8 Page (8 x 4096) to hold the Page structures.
	 * It should be enough to manage at most 128 MB (8 x 4096 x 4096) 
	 */
	 //_num_pages是实例用户可用的物理页数量
	_num_pages = (HEAP_SIZE / PAGE_SIZE) - 8;
	printf("HEAP_START = %x, HEAP_SIZE = %x, num of pages = %d\n", HEAP_START, HEAP_SIZE, _num_pages);
	
	struct Page *page = (struct Page *)HEAP_START;
	//初始化索引记录---每条索引记录对应一个用户可用物理页面
	for (int i = 0; i < _num_pages; i++) {
		_clear(page);
		page++;	
	}
    //物理页对齐4KB---将给定的地址按页面边界(4KB)对齐,确保地址位于所在页面的起始位置
	_alloc_start = _align_page(HEAP_START + 8 * PAGE_SIZE);
	//堆内存最大范围
	_alloc_end = _alloc_start + (PAGE_SIZE * _num_pages);

	printf("TEXT:   0x%x -> 0x%x\n", TEXT_START, TEXT_END);
	printf("RODATA: 0x%x -> 0x%x\n", RODATA_START, RODATA_END);
	printf("DATA:   0x%x -> 0x%x\n", DATA_START, DATA_END);
	printf("BSS:    0x%x -> 0x%x\n", BSS_START, BSS_END);
	printf("HEAP:   0x%x -> 0x%x\n", _alloc_start, _alloc_end);
}

//初始化过程就是将标志位清空
static inline void _clear(struct Page *page){
	page->flags = 0;
}
  • 保留堆内存前面8个物理页用于存放索引记录信息
  • 初始化相关索引信息
  • 堆内存分配起始地址页面对齐

注意: 此处出现的printf函数是在02小节中编写的printf.c文件中出现的,而非c语言提供的库函数,最终输出底层还是借助的上一节中编写uart.c代码,借助串口输出到连接设备的屏幕上。


连续分配多个物理页面:

/*
 * Allocate a memory block which is composed of contiguous physical pages
 * - npages: the number of PAGE_SIZE pages to allocate
 */
void *page_alloc(int npages)
{
	/* Note we are searching the page descriptor bitmaps. */
	int found = 0;
	//遍历索引数组
	struct Page *page_i = (struct Page *)HEAP_START;
	//_num_pages表示堆内存页面总数(用户可用堆内存--上面page_init函数中初始化过了)
	for (int i = 0; i <= (_num_pages - npages); i++) {
		//判断当前页面是否空闲
		if (_is_free(page_i)) {
			found = 1;
			/* 
			 * meet a free page, continue to check if following
			 * (npages - 1) pages are also unallocated.
			 */
			// 检查接下来的npages-1个物理页面是否同样空闲
			struct Page *page_j = page_i + 1;
			for (int j = i + 1; j < (i + npages); j++) {
			    //只要有一个物理页面不空闲,说明这块连续内存空间大小不满足我们的要求
				if (!_is_free(page_j)) {
				   //重新设置found=0
					found = 0;
					break;
				}
				page_j++;
			}
			/*
			 * get a memory block which is good enough for us,
			 * take housekeeping, then return the actual start
			 * address of the first page of this memory block
			 */
			//找到了满足要求的连续内存空间
			if (found) {
				//设置好相关物理页面对应的索引记录标志位为占用状态
				struct Page *page_k = page_i;
				for (int k = i; k < (i + npages); k++) {
					_set_flag(page_k, PAGE_TAKEN);
					page_k++;
				}
				//设置连续分配的页面中最后一个页面的flags标志位第1位为1,表示为当前分配中的最后一个物理页面
				page_k--;
				_set_flag(page_k, PAGE_LAST);
				//返回分配内存的起始地址
				return (void *)(_alloc_start + i * PAGE_SIZE);
			}
		}
		page_i++;
	}
	return NULL;
}

判断页面是否空闲和设置索引记录标记的函数如下:

static inline int _is_free(struct Page *page)
{
	if (page->flags & PAGE_TAKEN) {
		return 0;
	} else {
		return 1;
	}
}

static inline void _set_flag(struct Page *page, uint8_t flags)
{
	page->flags |= flags;
}

释放内存:

/*
 * Free the memory block
 * - p: start address of the memory block
 */
void page_free(void *p)
{
	/*
	 * Assert (TBD) if p is invalid
	 */
	//内存地址不合法或者超出的堆内存最大限制,直接返回
	if (!p || (uint32_t)p >= _alloc_end) {
		return;
	}
	/* get the first page descriptor of this memory block */
	struct Page *page = (struct Page *)HEAP_START;
	//定位对应的索引记录下标
	page += ((uint32_t)p - _alloc_start)/ PAGE_SIZE;
	/* loop and clear all the page descriptors of the memory block */
	//将对应page被占用的标记清空,同时如果是连续分配的最后一个页面,清空其PAGE_LAST标记
	while (!_is_free(page)) {
		if (_is_last(page)) {
			_clear(page);
			break;
		} else {
			_clear(page);
			page++;;
		}
	}
}

清空PAGE_LAST标志的函数如下:

static inline int _is_last(struct Page *page)
{
	if (page->flags & PAGE_LAST) {
		return 1;
	} else {
		return 0;
	}
}

调试

#include "os.h"

/*
 * Following functions SHOULD be called ONLY ONE time here,
 * so just declared here ONCE and NOT included in file os.h.
 */
extern void uart_init(void);
extern void page_init(void);

void start_kernel(void)
{
	uart_init();
	uart_puts("Hello, RVOS!\n");

	page_init();
    //页面分配测试
	page_test();
	while (1) {}; // stop here!
}


void page_test()
{
	void *p = page_alloc(2);
	printf("p = 0x%x\n", p);
	//page_free(p);

	void *p2 = page_alloc(7);
	printf("p2 = 0x%x\n", p2);
	page_free(p2);

	void *p3 = page_alloc(4);
	printf("p3 = 0x%x\n", p3);
}

输出:
在这里插入图片描述


扩展

可尝试基于课程02节已有的Page.c扩展出类似C语言中提供的malloc和free函数。

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

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

相关文章

qrcode.min.js下载

目录 qrcode.min.js下载步骤&#xff1a; 去官网 下载后解压&#xff1a; 如下&#xff1a;就可以得到 qrcode.min.js文件了 qrcode.min.js下载步骤&#xff1a; 去官网 可以前往qrcode官网&#xff08;https://davidshimjs.github.io/qrcodejs/&#xff09;下载qrcode.m…

培训pytorch(未完善) bp算法原理

生物的神经元 接受外部的刺激 神经网络工作流程演示

MySQL高可用集群解决方案之:MySql Cluster搭建

MySQL是当前使用最广泛的关系型数据库管理系统之一&#xff0c;但MySQL在高并发访问和大量数据处理方面存在较为明显的性能瓶颈。为了解决MySQL单点故障带来的不便和可扩展性问题&#xff0c;我们需要构建稳定性极高的MySQL集群方案。本文将介绍一种快速搭建MySQL集群的解决方案…

机器学习聚类算法——BIRCH算法、DBSCAN算法、OPTICS算法

系列文章目录 前言 本文主要介绍BIRCH算法、DBSCAN算法、OPTICS算法&#xff0c;以及相关案例举例&#xff0c;以下案例仅供参考 一、BIRCH算法 1.1、BIRCH算法简介 BIRCH&#xff08;Balanced Iterative Reducing and Clustering using Hierarchies&#xff0c;平衡迭代规约…

Kubernetes(K8s)各组件白话解释

深入springboot怎么启动tomcat NameSpacePodDeployment1. 创建多个副本2. 扩容/缩容3. 滚动更新4. 版本回退 ServiceClusterIpNodePort Ingress 个人笔记&#xff0c;仅供参考。 来源于尚硅谷视频 转载请评论。 NameSpace 名称空间&#xff0c;在K8s中用于隔离资源。对不同资源…

第十一届“泰迪杯”数据挖掘挑战赛成绩公示

第十一届“泰迪杯” 数据挖掘挑战赛成绩公示 第十一届“泰迪杯”数据挖掘挑战赛历时两个月顺利结束。竞赛采用盲审&#xff08;屏蔽参赛者信息&#xff1b;评审专家只能评阅非本区域作品&#xff1b;三位评阅专家同时评阅同一作品&#xff0c;超限调整后再取平均分&#xff0…

【小米的技术分享】数据库索引原理与底层数据结构解析

大家好&#xff0c;我是小米。今天我们来探讨一下数据库索引原理以及底层索引数据结构&#xff0c;同时还会介绍叶子节点存储的内容以及索引失效的情况。废话不多说&#xff0c;让我们开始吧&#xff01; IO操作与索引 首先&#xff0c;我们先来了解一下IO操作对于数据库索引的…

【区块链 | L2】作为Layer2赛道的领跑者,如何理解 Arbitrum?

上周我们介绍了以太坊L2扩展解决方案Optimism,本周我们继续介绍另一个L2解决方案——Arbitrum。Arbitrum 是以太坊的一个 Optimistic Rollup L2 可扩展性解决方案。 Part.1 什么是Arbitrum? Arbitrum 是一个构建在以太坊之上的区块链网络。你可以使用 Arbitrum 链来做任何在…

长短连接对压测的影响有多大?!

【引言】 当我们进行压力测试时&#xff0c;长短连接是一个非常重要的参数。但是&#xff0c;你知道吗&#xff1f;长短连接对于压测结果有着非常大的影响&#xff01;如果你不理解这个参数&#xff0c;那么你的压测结果可能会出现严重的偏差。 在这篇文章中&#xff0c;我将…

快速在linux上配置python3.9的环境以及可能报错的解决方案(python其它版本可同样方式安装)

一. linux安装python3.9 步骤1&#xff1a;安装系统依赖&#xff08;重要&#xff09; 这一步不执行&#xff0c;后面各种错误。 yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-d…

C#:不同登录用户阅读状态的修改:不同的登录用户,登录用户修改阅读状态后,状态变为已读,未修改阅读状态的用户,依然显示未读的程序实例

一、创建数据库 首先&#xff0c;可以使用SQL Server Management Studio创建一个新的数据库(MyDatabase) 以及三个表(Users, Messages, UserMessageStatus)&#xff0c;分别用于存储用户信息&#xff0c;消息信息以及用户消息状态。 Users表 该表包含了所有注册用户的信息。…

Es elasticsearch 十七 Java api 实现聚合 几个聚合示例 sql 开启许可 新特效 java 实现es7 sql 功能

目录 Java api 实现聚合 依赖 简单聚合按照颜色分组获取每个卖出数量 聚合每个颜色卖出数量&#xff0c;及平均价格&#xff08;每个分桶子聚合&#xff09; 按照颜色分组 &#xff0c;获取销售数量&#xff0c;avg min max sum 按照600区间价格范围分组&#xff0c;算出…

你学习web安全的路线可以介绍是吗

学习网络安全一定要选好一个方向去下功夫&#xff0c;如果不清楚自己究竟适合什么方向&#xff0c;可以都稍微尝试一下再来做选择&#xff0c;实战能力才是重点&#xff0c;所以你最应该找平台&#xff0c;多多实战&#xff0c;完成实际项目&#xff01; 书籍推荐&#xff1a;…

『赠书活动 | 第九期』清华社赞助 | 《Java系列丛书》

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; 『赠书活动 &#xff5c; 第九期』 本期书籍&#xff1a;《Java系列丛书》 公众号赠书&#xff1a;第一期 参与方式&#xff1a;关注公众号&#xff1a;低调而奢华 …

react-router V6 传递参数的三种方式

react-router V6 传递参数的三种方式 路由跳转使用navigate()路由传参1&#xff0c;searchParams传参2&#xff0c;params 传参3&#xff0c;state传参总结 路由跳转使用navigate() 编程式导航 导入一个 useNavigate钩子函数执行 useNavigate 函数得到 跳转函数在事件中执行跳…

服务器PING值不稳定是什么情况?

​  服务器ping值不稳定是指服务器在不同时间段内&#xff0c;对同一IP地址进行ping测试时&#xff0c;返回的延迟时间不同&#xff0c;存在波动的情况。这种情况会对服务器的性能和稳定性产生影响&#xff0c;影响用户的使用体验。下面我们来分析一下服务器ping值不稳定的原…

模拟IC方向面试常考问题及答案汇总,IC人必看

有不少小伙伴说想了解模拟IC方向的面试题目&#xff0c;这不就来了&#xff01;&#xff08;文末可领全部面试题目&#xff09; 1. 基尔霍夫定理的内容是什么&#xff1f; 电流定律&#xff1a;在集总电路中&#xff0c;任何时刻&#xff0c;对任一节点&#xff0c;所有流出节…

怎样让你的客户服务更高效

随着商业环境的变化&#xff0c;越来越多的企业开始意识到客户服务对于企业的重要性。高效的客户服务能够促进客户满意并增加销售额&#xff0c;从而帮助企业获得更大的成功。但是&#xff0c;怎样才能让您的客户服务更高效呢&#xff1f;以下是一些建议。 建立一个完善的客户…

浅水域三维探地雷达数值模拟研究

Gprmax浅水域三维地质雷达数值模拟研究 前言 浅水域地下不良地质体的探测一直是工程勘察的难点&#xff0c;地质雷达具有仪器轻便、操作简洁、分辨率高的优势&#xff0c;在浅水域勘察中具有很大的应用前景。目前&#xff0c;二维地质雷达已经有不少应用&#xff0c;三维地质…

面试专题:设计模式

面试时常见的就是的就是让你手写一个单例模式&#xff08;注意单例模式的几种不同的实现方法&#xff09;或者让你说一下某个常见的设计模式在你的项目中是如何使用的&#xff0c;另外面试官还有可能问你抽象工厂和工厂方法模式的区别、工厂模式的思想这样的问题。 建议把代理模…