MIT6.s081 2021 Lab Traps

使用gdb调试xv6内核

从最近两个 Lab 开始,代码逻辑的复杂度明显上升,对内核进行调试可能是帮助理解操作系统机制的绝佳方法。因此在开始本 Lab 之前,我们先来配置一下针对 xv6 内核的 gdb 调试器。

  1. 安装 gdb-multiarch.

利用包管理工具进行安装,我使用的是 Ubuntu 系统,执行以下命令:

sudo apt install gdb-multiarch
  1. 在 xv6 项目根目录下可以看到 .gdbinit 文件,其中已经写好了一些 gdb 的初始化选项,使用文本编辑器或 cat 命令查看:
set confirm off                                                         
set architecture riscv:rv64                                             
target remote 127.0.0.1:26000                                           
symbol-file kernel/kernel                                               
set disassemble-next-line auto           
set riscv use-compressed-breakpoints yes
  1. ~/.config/gdb/ 目录下的文件 gdbinit 中(没有则新建)添加安全加载路径,否则可能无法加载 .gdbinit 的配置。
add-auto-load-safe-path <xv6项目的根目录>/.gdbinit
  1. 打开两个终端窗口(可以使用 tmux 进行分屏),都需要进入 xv6 根目录,第一个窗口输入 make-qemu 等待调试器连接,第二个窗口输入 gdb-multiarch 打开 gdb,如果前面配置正确,那么 gdb 并自动加载 .gdbinit 配置,与 qemu 连接,之后便可以开始正常调试了。

在这里插入图片描述

RISC-V assembly

一些有关 RISC-V 汇编的问题,最好先通过网上博客或手册简单了解一下 RISC-V 的基本指令。

Q1:

Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf?

A1:

可以参考 RISC-V 的 calling conventiona0 - a7: 这些寄存器用于传递函数的前八个整数或指针类型的参数,如果超出这些寄存器的数量,超出的部分会存放在栈上。观察指令 li a2,13 可知,13 作为 printf 的第二个参数,存放在寄存器 a2 中。

在这里插入图片描述

Q2:

Where is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.)

A2:

调用函数 f 和函数 g 的代码被编译器优化,直接计算出了结果 12,作为 printf 的参数存入寄存器 a1 中:

26:   45b1                    li  a1,12

Q3:

At what address is the function printf located?

A3:

位于 0x638 地址处。

Q4:

What value is in the register ra just after the jalr to printf in main?

A4: 参考 riscv-calling,ra 用来存储函数调用的返回地址,因此 ra 的值为 jalr 1544(ra) 的后一条指令地址,即 0x38.

Q5:

Run the following code.

unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);

What is the output? Here’s an ASCII table that maps bytes to characters.

The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?

Here’s a description of little- and big-endian and a more whimsical description.

A5:

  • %x 用于输出一个无符号十六进制整数。
  • %s 用于输出一个字符指针所指向的字符串,直到遇到空字符\0为止。

小端模式下,57616 的 十六进制表示为 e110,&i 首地址开始的字节分别为 0x72, 0x6c, 0x64, 0x0,对应 ASCII 表中的字符为 r, l, d,因此最终输出结果为 He110 World.

若采取大端模式,i 的值应当替换为 0x726c6400,57616 的值无需改变,因为十六进制的书写规则并没有改变(高位在左,低位在右)。

Q6:

In the following code, what is going to be printed after 'y='? (note: the answer is not a specific value.) Why does this happen?

printf("x=%d y=%d", 3);

A6:

关于可变参数的内容查看 《C Programming Language 2nd Edition》(K&R)的 7.3 节 Variable-length Argument Lists.

简而言之,这样的操作将引发未定义行为,此时 ap 指向了一个未知的内存区域,并将该区域的数据以整型的形式输出。

Backtrace

思路

思路其实很简单:对照 lecture notes 给出的栈的结构,从当前栈帧的起始地址 fp 开始,fp - 8 的位置存放着当前函数调用的返回地址(上一次函数调用处的下一条指令地址),即我们 需要打印 的地址,fp - 16 的位置存放着上一次函数调用所在栈帧的起始地址,将该地址作为新的 fp 重复上述步骤即可。

在这里插入图片描述

关键问题是 什么时候停止 ?可以看到上述 backtrace 的过程就好像是在遍历一个链表,当链表的 next 域为空指针时链表到达末尾,那 traceback 完成后fp 的值应该是什么?为了寻找这个问题的答案,我选择先不设置终止条件,让它一直向上搜索,最后发现,返回地址最终为一个很小的值,这个地址显然不是我们想要的,在此之前应该退出,即本次 traceback 的尽头是 0x80001c92.

在这里插入图片描述

但打印出来的函数调用的返回地址似乎并没有什么规律,因此我又尝试将遍历过程中的栈帧起始地址 fp 打印出来,得到以下结果:

在这里插入图片描述

结合提示:

Xv6 allocates one page for each stack in the xv6 kernel at PAGE-aligned address.

原因就很明显了,在打印第三个返回地址时,此时栈帧起始地址为 0x3fffffa000,注意该地址后 12 二进制数为 0,且页面大小为 4KB,因此该地址位于一个页面的起始地址。又因为 xv6 内核只为每个 内核栈 分配一个页面的存储空间,该页面的起始地址按页面大小对齐,所以此时已经到达一个内核栈的顶端,无需继续遍历。

弄清楚了这些,代码的编写就很简单了:

void backtrace(void) {printf("backtrace:\n");uint64 fp = r_fp();uint64 top = PGROUNDUP(fp);do {printf("%p\n", *(uint64 *)(fp - 8));fp = *(uint64 *)(fp - 16);} while (fp < top); // reach the top of kernel stack
}

代码

--- a/kernel/defs.h
+++ b/kernel/defs.h
@@ -80,6 +80,7 @@ int             pipewrite(struct pipe*, uint64, int);void            printf(char*, ...);void            panic(char*) __attribute__((noreturn));void            printfinit(void);
+void			backtrace(void); // here// proc.cint             cpuid(void);
diff --git a/kernel/printf.c b/kernel/printf.c
index e1347de..a068cbd 100644
--- a/kernel/printf.c
+++ b/kernel/printf.c
@@ -114,6 +114,23 @@ printf(char *fmt, ...)release(&pr.lock);}+// here
+void backtrace(void) {
+	printf("backtrace:\n");
+	uint64 fp = r_fp();
+	uint64 top = PGROUNDUP(fp);
+
+	do {
+		printf("%p\n", *(uint64 *)(fp - 8));
+		fp = *(uint64 *)(fp - 16);
+	} while (lower < top);
+}
+voidpanic(char *s){
diff --git a/kernel/riscv.h b/kernel/riscv.h
index 1691faf..fae7bf3 100644
--- a/kernel/riscv.h
+++ b/kernel/riscv.h
@@ -331,6 +331,15 @@ sfence_vma()asm volatile("sfence.vma zero, zero");}+// here
+static inline uint64
+r_fp()
+{
+  uint64 x;
+  asm volatile("mv %0, s0" : "=r" (x) );
+  return x;
+}
+#define PGSIZE 4096 // bytes per page#define PGSHIFT 12  // bits of offset within a page
diff --git a/kernel/sysproc.c b/kernel/sysproc.c
index e8bcda9..f27c007 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -70,6 +70,7 @@ sys_sleep(void)sleep(&ticks, &tickslock);}release(&tickslock);
+  backtrace(); // herereturn 0;}

Alarm

思路

目前为止感觉最复杂的一题,需要对 trap 机制有一个比较深入的理解,建议在上手之前先仔细阅读与 trap 有关的代码:kernel/trampoline.Skernel/trap.c,这里也推荐一位博主写的两篇有关 xv6 的 trap 机制的博客:

6.S081——陷阱部分(一文读懂xv6系统调用)——xv6源码完全解析系列(5)

6.S081——补充材料——RISC-V架构中的异常与中断详解

test0: invoke handler

我们不妨按照提示的顺序来进行,不关注 sys_sigreturn,先把 sys_sigalarm 的功能实现。

实际上,sys_sigalarm 函数的功能很简单,只是简单地将用户态下传递的参数 tickshandler 存入进程的 struct proc 结构体中。实现调用 handler 的操作需要在内核态下的 usertrap 中完成,具体来说,针对时钟中断导致的 trap 将在 if(which_dev == 2) 后的语句中被处理。有两个目标需要完成: 定时函数调用

定时的逻辑比较清楚,在 struct proc 中添加变量 ticksum,代表从上次 handler 处理完成开始进程累计的时钟中断次数,该变量在进程初始化时设置为 0,随后每次遇到时钟中断,都自增 1,如果自增后的值达到了设定的间隔 ticks,则将其复位为 0,调用 handler 函数。

函数调用是一个需要考虑的问题,这里不能直接利用函数指针 handler 进行函数调用,因为 handler 指向的函数位于用户空间下,而 usertrap 位于内核态下,页表的地址映射不同,无法直接根据用户空间下的虚拟地址进行寻址(直接调用引发的错误如下图所示),需要在本次中断结束返回到用户态之后执行。因此正确的做法应该是设置进程 struct procepc 寄存器为函数指针 handler,这样在中断处理完成,进程回到用户态并被 CPU 调度执行后,寄存器 pc 将被设置预先保存的 epc 的值,这样函数 handler 就被成功调度执行了。至此,test0 应该成功通过。

在这里插入图片描述

在进入到 test1&2 之前,有必要说一说我的一些思考:在上面的讨论中,我们知道内核无法直接根据函数指针 handler 的值进行用户空间函数的调用,那能否在内核态下根据进程的用户态页表和给定的虚拟地址,利用软件地址转换机制(vm.c 中的 walkaddr 函数)来将用户空间的虚拟地址转换为物理地址进行寻址呢(这也是我最开始的想法)?答案是不行,因为即便是在内核态下,程序中的地址仍然是虚拟地址,也就是说即便知道用户态函数实际存储的物理地址,我们也只有在 给出一个虚拟地址,该虚拟地址经过内核页表地址转换之后,刚好得到了正确的物理地址, 才可能成功。而实际上,尽管内核 KERNBASEPHYSTOP 地址都是直接映射,但内核页表中可能并没有所需要的页表项,因此,这并不会成功。

test1/test2(): resume interrupted code

test1 的目标是,存储和恢复中断处理前后的寄存器状态。那么问题就来了:为什么需要存储这些寄存器?需要存储哪些寄存器?

其实最开始,我是有些纠结寄存器状态的存储目的是什么,认为可能是与内核态和用户态切换有关,但仔细想想,这部分的工作应该是由 trampoline.Susertrapret 来完成的,那么为什么还需要存储和恢复寄存器?

事实上,在系统未关闭中断的情况下,时钟中断可能在程序执行的任何时刻发生,且在返回到原程序位置继续执行之前还需要执行预先设定好的 handler 函数,那么寄存器状态的保存将是必要的。一方面在执行 handler 函数期间,如果 handler 函数包含一些对局部变量的处理,那么通用寄存器的值将会发生改变,从而使得中断返回时程序的执行结果与预期不符;另一方面,由于 epc 的值被手动改变,如果执行完 handler 之后不恢复中断发生时的保存的 pc 值,那么 pc 将会指向 handler 函数末尾的下一条指令,中断因此无法正常返回。 简单来说,这部分的操作相当于手动模拟了 线程 的切换。

另一个问题是:需要存储哪些寄存器?好吧,在解决这个 Lab 时我其实偷了点懒,没有去仔细琢磨,只是简单地将整个 trapframe 中所有的寄存器都保存下来。但根据上面的讨论,再结合 RISC-V 的 calling convention,应该不难得出答案。

最后的 test2 就比较简单了,目标是:

Prevent re-entrant calls to the handler----if a handler hasn’t returned yet, the kernel shouldn’t call it again.

解决的办法有很多,可以额外在 strcut proc 添加一个变量,用来表示进程当前是否正处在处理 handler 的过程中,如果是,则不进行 ticksum 的自增操作。这里我采用了一点 小技巧 :不添加额外的变量,而是在处理 handler 前将 ticksum 置为负数,并在自增前判断 ticksum 是否非负,在 sys_sigreturn 时再将它置为 0,本质上与添加变量的操作大差不差。

代码

diff --git a/Makefile b/Makefile
index 7a7e380..bc4d47a 100644
--- a/Makefile
+++ b/Makefile
@@ -188,6 +188,7 @@ UPROGS=\$U/_grind\$U/_wc\$U/_zombie\
+	$U/_alarmtest\diff --git a/kernel/proc.c b/kernel/proc.c
index 22e7ce4..80096f7 100644
--- a/kernel/proc.c
+++ b/kernel/proc.c
@@ -119,6 +119,7 @@ allocproc(void)found:p->pid = allocpid();p->state = USED;
+  p->ticksum = 0; // here// Allocate a trapframe page.if((p->trapframe = (struct trapframe *)kalloc()) == 0){
diff --git a/kernel/proc.h b/kernel/proc.h
index f6ca8b7..c1d5a23 100644
--- a/kernel/proc.h
+++ b/kernel/proc.h
@@ -105,4 +105,10 @@ struct proc {struct file *ofile[NOFILE];  // Open filesstruct inode *cwd;           // Current directorychar name[16];               // Process name (debugging)
+
+  int ticks;         // here
+  void (*handler)();
+  int ticksum;
+
+  struct trapframe strapframe;};
diff --git a/kernel/syscall.c b/kernel/syscall.c
index c1b3670..d4e5585 100644
--- a/kernel/syscall.c
+++ b/kernel/syscall.c
@@ -104,6 +104,8 @@ extern uint64 sys_unlink(void);extern uint64 sys_wait(void);extern uint64 sys_write(void);extern uint64 sys_uptime(void);
+extern uint64 sys_sigalarm(void); // here
+extern uint64 sys_sigreturn(void);static uint64 (*syscalls[])(void) = {[SYS_fork]    sys_fork,
@@ -127,6 +129,8 @@ static uint64 (*syscalls[])(void) = {[SYS_link]    sys_link,[SYS_mkdir]   sys_mkdir,[SYS_close]   sys_close,
+[SYS_sigalarm]  sys_sigalarm,  // here
+[SYS_sigreturn] sys_sigreturn,};void
diff --git a/kernel/syscall.h b/kernel/syscall.h
index bc5f356..a040610 100644
--- a/kernel/syscall.h
+++ b/kernel/syscall.h
@@ -20,3 +20,5 @@#define SYS_link   19#define SYS_mkdir  20#define SYS_close  21
+#define SYS_sigalarm  22  // here
+#define SYS_sigreturn 23
diff --git a/kernel/sysproc.c b/kernel/sysproc.c
index f27c007..ee859ed 100644
--- a/kernel/sysproc.c
+++ b/kernel/sysproc.c
@@ -96,3 +96,28 @@ sys_uptime(void)release(&tickslock);return xticks;}
+
+// here
+uint64 sys_sigalarm(void) {
+	struct proc *p = myproc();
+
+	if (argint(0, &(p->ticks)) < 0) {
+		return -1;
+	}
+
+	if (argaddr(1, (uint64 *)&(p->handler)) < 0) {
+		return -1;
+	}
+
+	return 0;
+}
+
+uint64 sys_sigreturn(void) {
+	struct proc *p = myproc();
+	
+	// restore registers
+	memmove(p->trapframe, &(p->strapframe), sizeof(p->strapframe));
+
+	p->ticksum = 0;
+	return 0;
+}
diff --git a/kernel/trap.c b/kernel/trap.c
index a63249e..447e6d8 100644
--- a/kernel/trap.c
+++ b/kernel/trap.c@@ -77,8 +77,17 @@ usertrap(void)exit(-1);// give up the CPU if this is a timer interrupt.
-  if(which_dev == 2)
+  if(which_dev == 2) {
+	// here
+	if (p->ticks > 0 && p->ticksum >= 0 && ++(p->ticksum) >= p->ticks) {
+	  // save registers
+	  memmove(&(p->strapframe), p->trapframe, sizeof(p->strapframe));
+
+	  p->ticksum = -1; // prevent re-entrant calls to the handler
+	  p->trapframe->epc = (uint64)p->handler;
+	}yield();
+  }usertrapret();}
diff --git a/user/user.h b/user/user.h
index b71ecda..422a4c1 100644
--- a/user/user.h
+++ b/user/user.h
@@ -23,6 +23,8 @@ int getpid(void);char* sbrk(int);int sleep(int);int uptime(void);
+int sigalarm(int ticks, void (*handler)()); // here
+int sigreturn(void);// ulib.cint stat(const char*, struct stat*);
diff --git a/user/usys.pl b/user/usys.pl
index 01e426e..84c6784 100755
--- a/user/usys.pl
+++ b/user/usys.pl
@@ -36,3 +36,5 @@ entry("getpid");entry("sbrk");entry("sleep");entry("uptime");
+entry("sigalarm"); # here
+entry("sigreturn");

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

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

相关文章

Linux基础:一. 简单的命令

文章目录 一. 简单的命令1.1 关机1.2 重启1.3 控制台打印工作目录1.4 切换当前目录1.5 列出当前目录中的目录和文件1.6 列出指定目录中的目录和文件1.7 控制台清屏1.8 查看和设置时间1.8.1 查看时间1.8.2 设置时间&#xff0c;需要管理员权限 一. 简单的命令 1.1 关机 comman…

【云原生】Prometheus监控Docker指标并接入Grafana

目录 一、前言 二、docker监控概述 2.1 docker常用监控指标 2.2 docker常用监控工具 三、CAdvisor概述 3.1 CAdvisor是什么 3.2 CAdvisor功能特点 3.3 CAdvisor使用场景 四、CAdvisor对接Prometheus与Grafana 4.1 环境准备 4.2 docker部署CAdvisor 4.2.2 docker部署…

Java 基础知识之 switch 语句和 yield 关键字

传统 switch 语句 传统的 switch 语句我们已经写了一万遍了&#xff0c;以下是一个典型的 switch 语句&#xff1a; int dayOfWeek 3; switch (dayOfWeek) {case 1:System.out.println("星期一");break;case 2:System.out.println("星期二");break;case…

【VUE基础】VUE3第一节—vite创建vue3工程

什么是VUE Vue (发音为 /vjuː/&#xff0c;类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建&#xff0c;并提供了一套声明式的、组件化的编程模型&#xff0c;帮助你高效地开发用户界面。无论是简单还是复杂的界面&#xff0…

MPS---MPQ86960芯片layout设计总结

MPQ86960 是一款内置功率 MOSFET 和栅极驱动的单片半桥。它可以在宽输入电压 (VIN) 范围内实现高达 50A 的连续输出电流 (IOUT)&#xff0c;通过集成MOSFET 和驱动可优化死区时间 (DT) 并降低寄生电感&#xff0c;从而实现高效率。 MPQ86960 兼容三态输出控制器&#xff0c;另…

ctfshow-web入门-文件上传(web151-web160)

目录 1、web151 2、web152 3、web153 4、web154 5、web155 6、web156 7、web157 8、web158 9、web159 10、web160 1、web151 试了下前端只能传 png 后缀的 将一句话木马改成 png 后缀&#xff0c;上传后用 burpsuite 抓包 绕过前端检测后&#xff0c;改回 php 后缀&am…

亚信安全发布2024年6月威胁态势,高危漏洞猛增60%

近日&#xff0c;亚信安全正式发布《2024年6月威胁态势报告》&#xff08;以下简称“报告”&#xff09;&#xff0c;报告显示&#xff0c;6月份新增信息安全漏洞 1794个&#xff0c;高危漏洞激增60%&#xff0c;涉及0day漏洞占67.67%&#xff1b;监测发现当前较活跃的勒索病毒…

【python】python客户信息审计风险决策树算法分类预测(源码+数据集+论文)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

仿写Spring IOC

文章目录 IOC思路手写代码思路一些杂七杂八&#xff08;可跳过&#xff09; IOC思路 手写代码思路 手写代码的思路: 自定义一个MyAnnotationConfigApplicationContext&#xff0c;构造器中传入要扫描的包。获取这个包下的所有类。遍历这些类&#xff0c;找出添加了Component注…

智慧水利的变革之路:如何通过大数据、物联网和人工智能构建高效、智能、可持续的水利管理新模式

目录 一、引言&#xff1a;智慧水利的时代背景与意义 二、大数据&#xff1a;水利管理的数据基石 &#xff08;一&#xff09;数据收集与整合 &#xff08;二&#xff09;数据分析与挖掘 三、物联网&#xff1a;水利管理的感知神经 &#xff08;一&#xff09;智能感知与监…

14-22 剑和远方2 - 深度神经网络中的学习机制

概论 在第一部分中&#xff0c;我们深入探讨了人工智能的兴衰简史以及推动人工智能发展的努力。我们研究了一个简单的感知器&#xff0c;以了解其组件以及简单的 ANN 如何处理数据和权重层。在简单的 ANN 中&#xff0c;不会对数据执行特定操作。ANN 中的激活函数是一个线性函…

【面试八股文】java基础知识

引言 本文是java面试时的一些常见知识点总结归纳和一些拓展&#xff0c;笔者在学习这些内容时&#xff0c;特地整理记录下来&#xff0c;以供大家学习共勉。 一、数据类型 1.1 为什么要设计封装类&#xff0c;Integer和int区别是什么&#xff1f; 使用封装类的目的 对象化:…

MPC学习资料汇总

模型预测控制MPC学习资料汇总 需要的私信我~ 需要的私信我~ 需要的私信我~ 【01】课件内容 包含本号所有MPC课程的课件&#xff0c;以及相关MATLAB文档。 【02】课件源代码 本号所有MPC课程的源代码。 【03】MPC仿真案例 三个MPC大型仿真案例&#xff1a; 1&#xff09;…

纯前端低代码开发脚手架 - daelui/molecule

daelui/molecule低代码开发脚手架&#xff1a;分子组件开发、预览、打包 页面代码示例、大屏代码示例预览 可开发页面组件 可开发大屏组件 项目git地址&#xff1a;https://gitee.com/daelui/molecule 在线预览&#xff1a;http://www.daelui.com/daelui/molecule/app/index.…

Codeforces Round 955 E. Number of k-good subarrays【分治、记忆化】

E. Number of k-good subarrays 题意 定义 b i t ( x ) bit(x) bit(x) 为 x x x 的二进制表示下 1 1 1 的数量 一个数组的子段被称为 k − g o o d k-good k−good 的当且仅当&#xff1a;对于这个子段内的每个数 x x x&#xff0c;都有 b i t ( x ) ≤ k bit(x) \leq k…

ubuntu24.04按关键字卸载不需要的apt包

使用的时候发现一个imagemagic无法正常读取文件&#xff0c;试图卸载 man apt经过尝试后&#xff0c;发现list的一个神奇关键字&#xff0c;用来显示已安装的软件包 sudo apt list --installed | grep image按image关键字过滤&#xff1a; 之后按软件名卸载即可 sudo apt pu…

30多款简洁个人博客网站网页模板演示学习

30多款个人博客个人网站divcss,html在线预览,静态页面模板免费下载.这些简洁和优雅的博客网页模板,为那些想成为创建博客的个人或媒体提供灵感设计。网页模板可以记录旅游、生活方式、食品或摄影博客等网站。 http://www.bokequ.com/blog/1/ http://www.bokequ.com/blog/2/ htt…

vue事件处理v-on或@

事件处理v-on或 我们可以使用v-on指令&#xff08;简写&#xff09;来监听DOM事件&#xff0c;并在事件触发时执行对应的Javascript。用法&#xff1a;v-on:click"methodName"或click"hander" 事件处理器的值可以是&#xff1a; 内敛事件处理器&#xff1…

无人机有哪些关键技术?

一、控制技术 无人机的核心还是在控制上&#xff0c;飞控系统的可靠性、稳定性及可扩展性是其中重要的指标。可靠性上&#xff0c;除了器件选型之外&#xff0c;目前主要靠多余度来增加&#xff1b;稳定性主要体现在多场景下仍能保持良好的工作状态&#xff0c;主要靠算法来进…

轻松创建对象——简单工厂模式(Java实现)

1. 引言 大家好&#xff0c;又见面了&#xff01;在上一篇文章中&#xff0c;我们通过Python示例介绍了简单工厂模式&#xff0c;今天&#xff0c;我们继续深入这个话题&#xff0c;用Java来实现简单工厂模式。 2. 什么是简单工厂模式 简单工厂模式&#xff08;Simple Facto…