6.S081——Lab3——page table

news2024/11/14 13:31:12

0.briefly speaking

这是6.S081 Fall 2021的第三个实验,是页表和虚拟内存相关的实验,在之前我们已经详细地阅读了Xv6内核中有关虚拟内存管理的源代码,现在就可以深入一下这个实验了。本实验分为如下三个小任务:

  • Speed up system calls (easy)
  • Print a page table (easy)
  • Detecting which pages have been accessed (hard)

一个一个来看吧…

1.Speed up system calls (easy)

第一个任务是加速系统调用,主要做法就是在创建进程时在用户页表中额外分配一个只读的页面,这样就可以在执行特定系统调用时直接从用户页表中读出数据并返回减少在用户态和内核态之间的穿越次数,从而减少系统调用的开销,加速执行过程。

按照实验指导书上的提示,我们可以按照如下步骤来完成这个任务

1.在kernel/proc.c的allocproc函数中为进程分配一个物理内存页面,专门用来存放共享信息
我们首先阅读一下kernel/proc.c中的allocproc函数,看看它在做什么,然后仿照它的写法,在代码中添加分配一个加速页面的代码,这个页面在后面会由proc_pagetable函数映射到进程页表内。

// Look in the process table for an UNUSED proc.
// If found, initialize state required to run in the kernel,
// and return with p->lock held.
// If there are no free procs, or a memory allocation fails, return 0.
// 译:查找进程表,找到UNUSED状态的进程
// 如果找到了UNUSED状态的进程,那么初始化它的状态,使其能在内核态下运行
// 并且返回时不占用锁
// 如果没有空闲进程,或内存分配失败,则返回0
static struct proc*
allocproc(void)
{
  struct proc *p;
  
  // 遍历进程组,寻找处于UNUSED状态的进程
  for(p = proc; p < &proc[NPROC]; p++) {
    // 首先获取进程的锁,保证访问安全
    acquire(&p->lock);
    
    // 如果进程状态为UNUSED,则跳转至found
    if(p->state == UNUSED) {
      goto found;
    } else {
      // 否则释放锁,检查下一个进程是否为UNUSED状态
      release(&p->lock);
    }
  }
  return 0;

// 以下是初始化一个进程的代码
found:
  // 为当前进程分配PID,并将当前进程状态改为USED
  p->pid = allocpid();
  p->state = USED;

  // Allocate a trapframe page.
  // 分配一个trapframe页,如果不成功则释放当前进程和锁
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // <照葫芦画瓢,我们也像trapframe一样申请一个空闲物理页用来存储变量>
  // <那么在进程信息里也得保留一份指向这个页面的指针>,这里记为p->usyscall
  // 因为内核执行的是直接映射,所以kalloc返回的指针可以直接当作物理地址映射到页表中
  if((p->usyscall) == (struct usyscall *)kalloc() == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  
  // <将pid信息放入结构体,其实也就是放到了这个物理页面开头处>
  p->usyscall->pid = p->pid;

  // An empty user page table.
  // 为当前进程申请一个页表页,并将trapframe和trampoline页面映射进去
  // 注意trampoline代码作为内核代码的一部分,不用额外分配空间,只需要建立映射关系
  // <我们后面将要修改这个函数,将加速页面一并映射进去>
  p->pagetable = proc_pagetable(p);
  
  // 如果上述函数执行不成功,则释放当前进程和锁
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // Set up new context to start executing at forkret,
  // which returns to user space.
  // 译:设置新的上下文并从forkret开始执行
  // 这会回到用户态下
  // 这里涉及trap的返回过程,我们在后面研究源码时再深入解读这里的行为
  memset(&p->context, 0, sizeof(p->context));
  p->context.ra = (uint64)forkret;
  p->context.sp = p->kstack + PGSIZE;
 
  // 返回新的进程
  return p;
}

注意在上面我将与实验有关的注释全部用尖括号括出了,正如注释中所展示的,我们首先要修改的是对于进程结构体的定义,在其中添加一枚指针指向usyscall结构体,这枚指针本质上也指向了加速页面的起始物理地址,如下所示:

// Per-process state
struct proc {
  struct spinlock lock;

  // p->lock must be held when using these:
  enum procstate state;        // Process state
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  int xstate;                  // Exit status to be returned to parent's wait
  int pid;                     // Process ID

  // wait_lock must be held when using this:
  struct proc *parent;         // Parent process

  // these are private to the process, so p->lock need not be held.
  uint64 kstack;               // Virtual address of kernel stack
  uint64 sz;                   // Size of process memory (bytes)
  pagetable_t pagetable;       // User page table
  struct trapframe *trapframe; // data page for trampoline.S
  struct usyscall *usyscall;   // <加入一个指向加速页面的指针> 
  struct context context;      // swtch() here to run process
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)
};

然后在allocproc中我们为这枚指针分配了一个物理页,并将pid信息放入了这个页面,这些代码已经在上面完整写出了,在此不再赘述。

2.在freeproc函数中编写释放内存的代码,这是用于可能出现的错误处理
首先还是先阅读一下freeproc的代码实现,并在其中加入释放usyscall页面的代码:

// free a proc structure and the data hanging from it,
// including user pages.
// p->lock must be held.
// 译:释放进程结构体和悬挂在其上的数据,包括页表
// 必须要持有进程p的锁才可以调用此函数
static void
freeproc(struct proc *p)
{
  // 释放trapframe页面,之所以要单独释放是因为:
  // trapframe页面位于地址空间的最高处,与下面已经使用的地址空间是分离的
  if(p->trapframe)
    kfree((void*)p->trapframe);
  
  // 释放完将trapframe指针置为空
  p->trapframe = 0;
  
  // <仿照trapframe内存释放的代码,在出错时将p->usyscall页面释放,并将指针置空> 
  if(p->usyscall)
    kfree((void*)p->usyscall);
  p->usyscall = 0;
  
  // 释放页表
  // proc_freepagetable会调用uvmunmap解除trampoline和trapframe的映射关系
  // 并最终调用uvmfree释放内存和页表
  // <我们后面要修改此处的函数>
  if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  
  // 将页表与其他量全部置为空,表示进程已完全释放
  p->pagetable = 0;
  p->sz = 0;
  p->pid = 0;
  p->parent = 0;
  p->name[0] = 0;
  p->chan = 0;
  p->killed = 0;
  p->xstate = 0;
  p->state = UNUSED;
}

但在完成这些之后,仅仅释放了usyscall页面的物理内存,接下来还要在proc_freepagetable函数中解除一下页面的映射关系,这样的释放过程才算是完备,故修改proc_freepagetable函数如下:

// Free a process's page table, and free the
// physical memory it refers to.
// 译:释放进程的页表,并释放它指向的物理内存
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
  // 解除TRAMPOLINE和TRAPFRAME的映射关系
  // 之所以要分开来写是因为它们和连续的地址空间是分离的
  // 连续空间使用uvmfree一套解决
  uvmunmap(pagetable, TRAMPOLINE, 1, 0);
  uvmunmap(pagetable, TRAPFRAME, 1, 0);
  
  // <释放USYSCALL函数的映射关系>
  uvmunmap(pagetable, USYSCALL, 1, 0);
  uvmfree(pagetable, sz);
}

3.最后,在kernel/proc.c的proc_pagetable函数中完成这个页面的映射,并设置页面访问权限为用户态只读(PTE_U | PTE_R)

我们先阅读一下proc_pagetable的代码实现,然后将这个“加速页面”映射到页表中,代码如下:

// Create a user page table for a given process,
// with no user memory, but with trampoline pages.
// 译: 为给定的进程创建一个页表
// 没有用户地址空间,只有trampoline页面
pagetable_t
proc_pagetable(struct proc *p)
{
  pagetable_t pagetable;

  // An empty page table.
  // 调用uvmcreate函数返回一个空页表
  // uvmcreate函数的详细解释见完全解析系列博客(2)
  pagetable = uvmcreate();
  if(pagetable == 0)
    return 0;

  // map the trampoline code (for system call return)
  // at the highest user virtual address.
  // only the supervisor uses it, on the way
  // to/from user space, so not PTE_U.
  // 译:将trampoline页面(用于系统调用返回)映射到用户最高虚拟地址处
  // 只有超级用户(处于超级用户模式)下才可以使用
  // 所以PTE_U标志为0
  // mappages函数的讲解见系列博客(1)
  // uvmfree函数的讲解见系列博客(2)
  // 如果出错,就调用uvmfree来释放映射关系,回收页表
  // 注意这里传入的sz是0,表明在这一步没有实际的物理内存需要释放
  if(mappages(pagetable, TRAMPOLINE, PGSIZE,
              (uint64)trampoline, PTE_R | PTE_X) < 0){
    uvmfree(pagetable, 0);
    return 0;
  }
  
  // map the trapframe just below TRAMPOLINE, for trampoline.S.
  // uvmunmap函数的讲解见系列博客(2)
  // 将trapframe映射到紧邻TRAMPOLINE的下一个页面
  // 如果出错,首先取消TRAMPOLINE的映射关系,再使用uvmfree释放页表映射关系,回收页表
  if(mappages(pagetable, TRAPFRAME, PGSIZE,
              (uint64)(p->trapframe), PTE_R | PTE_W) < 0){
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }
  
  // <仿照上面的格式将加速页面映射到页表中>
  // 使用mappages将此页映射到页表中
  // 如果出错要释放映射trampoline和trapframe的映射关系
  // 并释放pagetable的内存,返回空指针
  if(mappages(pagetable, USYSCALL, PGSIZE, 
              (uint64)(p->usyscall), PTE_R | PTE_U) < 0){
      uvmunmap(pagetable, TRAMPOLINE, 1, 0);
      uvmunmap(pagetable, TRAPFRAME, 1, 0);
      uvmfree(pagetable, 0);
      return 0;
  }
  // <添加代码结束>

  return pagetable;
}

其实这个实验要做的事情不多,但其实非常考验对页表和虚拟内存机制的理解,让我们梳理一下这个小小的实验。

首先,allocproc函数会在调用kalloc分配一个物理页面专门用来存储一些不用进入内核态就可以直接读取的信息。kalloc会返回一个指针,虽是虚拟地址,但由于内核页表的直接映射机制,我们也可以将此地址作为物理地址直接使用。随后,我们将想要加速访问的数据(pid)直接放入此页面中,并在进程信息结构体中维持一个指向此页面的指针。注意直到此时,这个页面只是存在了,但是进程还没有将其映射到自己的地址空间中,因而在页表中是访问不到这个页面的

接下来allocproc会调用proc_pagetable函数,这个函数会创建进程的页表,并将特殊的几个页面(trampoline、trapframe、我们的加速页面)映射到页表中。这几个特殊页面必须已经分配好,即在物理内存中已经存在,我们在这里只是使用建立映射关系。trampoline是内核代码的一部分,始终在内存中存在,trapframe和我们的加速页面在之前的allocproc中已经分配完毕,故它们也是存在的,所以在proc_pagetable中我们使用mappages一一将它们映射到进程页表中。

在上述步骤都完成之后,一个进程在用户态下,直接访问所谓的USYSCALL虚拟地址,这个地址经过多级页表的翻译就会索引到我们分配的加速页面中,进而直接获取到了想要的数据。

上述是整个实验的原理,更加精妙的地方在于错误处理,即在上述过程中可能出现的错误处理及对应的处置措施。注意allocproc、proc_pagetable、proc_freepagetable、free_proc之间的分工和配合,不能错乱。

  • allocproc负责分配物理内存
  • freeproc负责释放物理内存
  • proc_pagetable负责申请页表页和建立映射关系
  • proc_freepagetable负责取消映射关系和回收页表页

值得玩味的是proc_pagetable函数中映射页面失败时,会返回一个空指针给allocproc,这里如果返回空指针给allocproc的话,后面会直接进入freeproc,且此时因为p->pagetable指针为空,所以proc_freepagetable函数不会执行,所以原本属于它的释放逻辑全部得写到proc_pagetable中作为补偿,所以我们看到其实proc_pagetable中的错误处理逻辑和proc_freepagetable的逻辑从形式上是十分相似的

运行一下测试程序,结果是正确的:

在这里插入图片描述

2.Print a page table (easy)

第二个小任务也很简单,就是将一个进程的页表按照指定格式打印出来。按照实验指导书所述,这个函数的格式应该和walkaddr非常相似,所以应该是一个递归的逻辑,所以我们就参照这个函数的格式来写。但是最困难的地方可能在于控制打印格式,所以我首先定义了raw_vmprint函数,这个函数的作用在于按照页表的层次打印指定数量的缩进符,代码如下(kernel/vm.c):

// 此函数借鉴了walkaddr的写法,用来递归地打印页表
void
raw_vmprint(pagetable_t pagetable, int Layer)
{
  // 遍历页表的每一项
  for(int i = 0 ; i < 512 ; ++i){
    pte_t pte = pagetable[i];
     
    // 如果当前的pte指向的是更低一级的页表
    if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
      // 从PTE中解析出物理地址,并打印指定数量的缩进符
      // 注意解析物理地址时,不能只是简单地将权限位移除出去,还应该左移12位,让出页内偏移量
      uint64 phaddr = (pte >> 10) << 12;
      for( ; Layer != 0 ; --Layer)
        printf(".. ");
      
      // 打印本级页表信息,向孩子页表递归,注意层数+1
      printf("..%d: pte %p pa %p\n", i, pte, phaddr);
      uint64 child = PTE2PA(pte);
      raw_vmprint((pagetable_t)child, Layer + 1);
    }
    
    // 如果当前PTE指向的是叶级页表
    // 取出物理地址并打印信息,随后返回
    else if (pte & PTE_V){
      uint64 phaddr = (pte >> 10) << 12;
      printf(".. .. ..%d: pte %p pa %p\n", i, pte, phaddr);
    }
  }
}

然后在此函数的基础上封装一层,就构成了最终的vmprint函数:

void vmprint(pagetable_t pagetable)
{
  raw_vmprint(pagetable, 0);
}

不要忘记在def.h函数中加上vmprint的函数签名,否则这个函数不能被其他函数调用:

// kernel/def.h:173
void            vmprint(pagetable_t);   // <插入vmprint的函数签名>

最后别忘了在exec函数返回之前调用vmprint函数:

// kernel/exec.c:119-122
// 为第一个进程打印页表,注意这个页表头也可以放在vmprint中打印
// 我为了让vmprint函数更加干净,把这个打印语句摘出来了
if(p->pid == 1){
    printf("page table %p\n", p->pagetable);
    vmprint(p->pagetable);
  }

这个小任务到此就结束了,非常简单,让我们看看具体的打印效果:
在这里插入图片描述
这些虚拟地址都应该和实验指导书上保持一致,而具体的物理地址可以随着实验环境的不同而不同。最后再测试一下打印的正确性,肯定是没有问题的:
在这里插入图片描述

3.Detecting which pages have been accessed (hard)

最后一个任务是实现一个系统调用来检测某个页面是否被访问的,其实也非常简单。按照实验指导书中的指示,我们首先在头文件kernel/riscv.h中将PTE_A加入头文件,查询一下RISC-V的指令手册,发现PTE_A这个标志在第6位
在这里插入图片描述
另外,我们规定一下一次最多可以查询的页面数量MAXSCAN,它同样也定义在kernel/riscv.h文件中(32这个数值是通过阅读user/pgtbltest.c中的pgaccess_test函数确认的)。

#define PTE_V (1L << 0) // valid
#define PTE_R (1L << 1)
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // 1 -> user can access
#define PTE_A (1L << 6) // <加入对访问位的支持>
#define MAXSCAN 32      // <限制一次最多可以查询的页面数量>

然后我们就可以来着手实现一下sys_pgaccess函数,按照指导书上的提示,这个函数的实现也是相对简单的,我的代码实现如下:

#ifdef LAB_PGTBL
// 在这里声明对kernel/vm.c/walk函数的引用声明
extern pte_t * walk(pagetable_t, uint64, int);

int
sys_pgaccess(void)
{
  // lab pgtbl: your code here.
  // 在内核态下声明一个BitMask,用来存放结果
  uint64 BitMask = 0;
  
  // 声明一些变量,用来接收用户态下传入的参数
  uint64 StartVA;
  int NumberOfPages;
  uint64 BitMaskVA;
  
  // 首先读取要访问的页面数量
  if(argint(1, &NumberOfPages) < 0)
    return -1;
  
  // 如果页面数量超过了一次可以读取的最大范围
  // 系统调用直接返回
  if(NumberOfPages > MAXSCAN)
    return -1;                    
  
  // 读取页面开始地址和指向用户态存放结果的BitMask的指针
  if(argaddr(0, &StartVA) < 0)
    return -1;
  if(argaddr(2, &BitMaskVA) < 0)
    return -1;

  int i;
  pte_t* pte;
	
  // 从起始地址开始,逐页判断PTE_A是否被置位
  // 如果被置位,则设置对应BitMask的位,并将PTE_A清空
  for(i = 0 ; i < NumberOfPages ; StartVA += PGSIZE, ++i){
    if((pte = walk(myproc()->pagetable, StartVA, 0)) == 0)
      panic("pgaccess : walk failed");
    if(*pte & PTE_A){
      BitMask |= 1 << i;	// 设置BitMask对应位
      *pte &= ~PTE_A;		// 将PTE_A清空
    }
  }
  
  // 最后使用copyout将内核态下的BitMask拷贝到用户态
  copyout(myproc()->pagetable, BitMaskVA, (char*)&BitMask, sizeof(BitMask));
  return 0;
}
#endif

所以这就是sys_pgaccess系统调用的实现,最后来测试一下它的正确性:
在这里插入图片描述

4.结语

至此,就完成了6.S081 Fall 2021的第三个实验,有关虚拟内存和页表机制的实验内容。总的来说实验难度都没有很大,在指导书的帮助下可以很快完成。但是对于内核代码的研究却远远没有结束,接下来的实验内容涉及到操作系统中最重要的一个部分,那就是终端和陷阱机制的实现。已经迫不及待去扒一下对应的源码了…哈哈

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

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

相关文章

【Mac-m2芯片docker安装AWVS及问题解决】

【Mac-m2芯片docker安装AWVS及问题解决】 docker安装AWVS安装报错问题解决 docker安装 docker安装命令&#xff1a; brew install --cask --appdir/Applications docker 查看是否安装成功&#xff1a; docker --version docker info 换源&#xff1a; “https://hub-mirror.c.…

在Python中使用牛顿法

牛顿法简介 牛顿法&#xff08;Newton’s method&#xff09;是一种常用的优化算法&#xff0c;在机器学习中被广泛应用于求解函数的最小值。其基本思想是利用二次泰勒展开将目标函数近似为一个二次函数&#xff0c;并用该二次函数来指导搜索方向和步长的选择。 牛顿法需要计…

从零开始学架构——FMEA故障模式与影响分析

1 FMEA介绍 FMEA&#xff08;Failure mode and effects analysis&#xff0c;故障模式与影响分析&#xff09;又称为失效模式与后果分析、失效模式与效应分析、故障模式与后果分析等&#xff0c;专栏采用“故障模式与影响分析”&#xff0c;因为这个中文翻译更加符合可用性的语…

高并发的程序设计-系统设计层面

高并发的程序设计-系统设计层面 目录概述需求&#xff1a; 设计思路实现思路分析1.主要指标二、处理高并发的方案 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a better …

ChatGPT写文章效果-ChatGPT写文章原创

ChatGPT写作程序&#xff1a;让文案创作更轻松 在当前数字化的时代&#xff0c;营销推广离不开文案创作。然而&#xff0c;写作对许多人来说可能是一项耗时而枯燥的任务。如果您曾经为写出较高质量的文案而苦恼过&#xff0c;那么ChatGPT写作程序正是为您而设计的。 ChatGPT是…

gitlab安装与使用(图文详解超详细)

一 找最新的安装镜像 推荐用清华源 目前最新版本是15.95 二 在/opt 下创建gitlab文件夹 [rootlocalhost ~]# mkdir /opt/gitlab [rootlocalhost ~]# 三 在gitlab目录下写一个 shell脚本 vim int.sh给它加上执行权限 chmod ux int.sh运行这个脚本 ./ins.sh出现这个截图 安…

three.js之摄像机

本节将在上一节的基础上进一步介绍一下摄像机功能。 three.js的摄像机主要包括两类&#xff1a;正交投影摄像机和透视投影摄像机。 透视投影摄像机&#xff1a;THREE.PerspectiveCamera&#xff0c;最自然的视图&#xff0c;距离摄像机越远&#xff0c;它们就会被渲染得越小。…

【编程问题】解决 mapper.xml 文件的 resultType 爆红问题:Cannot resolve symbol ‘xxx‘

解决mapper.xml文件的resultType爆红问题&#xff1a;Cannot resolve symbol xxx 1.问题描述2.问题分析3.问题解决3.1 配置注解&#xff08;推荐&#xff09;3.2 配置全类名3.3 删除插件 4.事件感悟 系统&#xff1a;Win10 JDK&#xff1a;1.8.0_333 IDEA&#xff1a;2022.2.4 …

HCIP之MPLS中的VPN

目录 HCIP之MPLS中的VPN 定义 例图 解读 流程 双层标签技术 控制层面 数据层面 配置 HCIP之MPLS中的VPN 定义 VPN --- 虚拟网专用 --- 是一种运营商提供的&#xff0c;专门解决虚拟专线安全及宽带问题的综合解决方案 例图 解读 站点 --- 可以理…

Day936.如何重构过大类 -系统重构实战

如何重构过大类 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于如何重构过大类的内容。 在过去的代码里一定会遇到一种典型的代码坏味道&#xff0c;那就是“过大类”。 在产品迭代的过程中&#xff0c;由于缺少规范和守护&#xff0c;单个类很容易急剧膨胀&#…

婴儿尿布台出口美国CPC认证

什么是尿布台&#xff1f;尿布台上架亚马逊要怎么做&#xff1f;咱们接着往下看。 什么是尿布台&#xff1f; 尿布台&#xff1a;尿布台是一种自立式抬升结构&#xff0c;通常设计用于承托体重不超过 13.61 千克&#xff08;30 磅&#xff09;的儿童。儿童采用平躺姿势&#…

Forest-声明式HTTP客户端框架-集成到SpringBoot实现调用第三方restful api并实现接口数据转换

场景 Forest 声明式HTTP客户端API框架&#xff0c;让Java发送HTTP/HTTPS请求不再难。它比OkHttp和HttpClient更高层&#xff0c; 是封装调用第三方restful api client接口的好帮手&#xff0c;是retrofit和feign之外另一个选择。 通过在接口上声明注解的方式配置HTTP请求接…

echarts中横坐标显示为time,使用手册

需求&#xff1a; 后端传递&#xff08;两段数据&#xff0c;不同时间间隔&#xff09;的24h实时数据&#xff0c;前端需要根据24小时时间展示&#xff0c;要求&#xff1a;x轴为0-24h&#xff0c;每个两小时一个刻度 误区&#xff1a; 刚开始通过二维数据的形式秒点&#xff…

Python入门到精通12天(迭代器与生成器)

迭代器与生成器 迭代器生成器 迭代器 迭代器是可迭代的对象&#xff0c;即可以进行遍历的对象。列表、字符串、元组、字典和集合这些都是可迭代的对象&#xff0c;都可以进行遍历。 迭代器是一种访问序列元素的方式&#xff0c;它可以通过next()函数逐个返回序列中的元素。并…

mybatis3源码篇(1)——构建流程

mybatis 版本&#xff1a;v3.3.0 文章目录 构建流程SqlSessionFactoryBuilderXMLConfigBuildertypeAliasesElementtypeHandlerElementmapperElementMapperRegistry MappedStatementMapperAnnotationBuilderXMLMapperBuilderMapperBuilderAssistant SqlSessionFactorySqlSession…

【录用案例】1区SCI仅36天录用,新增多本1-2区SCI,CNKI评职好刊发表案例

我处上周&#xff08;2023年4月8日-2023年4月14日&#xff09;经核实&#xff0c;由我处Unionpub学术推荐的24篇论文已被期刊部录用、20篇见刊&#xff0c;5篇检索&#xff1a; ✔新增1区纳米与环境类SCI&EI&#xff0c;仅36天录用&#xff0c;录用后17天见刊&#xff1b;…

前端canvas截图酷游地址的方法!

前情提要 想在在JavaScript中&#xff0c;酷游专员KW9㍠ㄇEㄒ提供用HTML5的Canvas元素来剪取画面并存成SVG或PNG。 程式写法(一) 首先&#xff0c;需要在HTML中创建一个Canvas元素<canvas id"myCanvas"></canvas> 在JavaScript中&#xff0c;使用canv…

【Java面试】ArrayList、LinkedList 查找数据哪个快

ArrayList、LinkedList查找数据哪个快 这里有几种不同情况 1、是不是有序的&#xff1f; 2、说的查找是什么意思&#xff1f;是调用get(1)&#xff0c;还是调用的contains(o)方法&#xff1f; 根据上面的问题&#xff0c;我们可以分开讨论&#xff1a; 1、数据是有序的 指定…

Apifox自动生成接口文档

1、安装 1.1 Apifox安装 官方文档&#xff1a;Apifox - API 文档、调试、Mock、测试一体化协作平台 - 接口文档工具&#xff0c;接口自动化测试工具&#xff0c;接口Mock工具&#xff0c;API文档工具&#xff0c;API Mock工具&#xff0c;API自动化测试工具 1.2 IDEA 插件安装…

Vue 复学 之 状态管理 Vuex

Vuex是vue中的一种状态管理模式&#xff0c;就是一个 状态仓库&#xff0c;仓库做什么&#xff1f;存储状态、管理状态&#xff08;数据&#xff09;的变化、提供状态获取窗口。 本文中一些测试用例基于vue/composition-api1.7.1 &#xff0c; vuex3.6.2&#xff0c; vue2.6.1…