6.1810: Operating System Engineering 2023 <Lab3: page tables>

一、本节任务

实验环境:

二、要点

 如何防止程序破坏内核或其他进程空间?隔离地址空间,进程只能读写自己的内存空间。

在保证隔离的同时,如何将多个地址空间复用到一个物理内存上?虚拟内存/页表。操作系统通过页表来为每个进程提供自己的私有地址空间和内存。

2.1 分页硬件

页表为寻址提供了一个间接的层次,CPU 通过虚拟地址(VA)访存,MMU 将虚拟地址映射成实际的物理地址(PA),再通过实际的物理地址去访问 RAM,这样做的主要目的是为了隔离(isolation),每个进程都能有自己的地址空间。

内核通过页表(page table)来告诉 MMU 如何将虚拟地址映射成物理地址。

如果我们需要每个进程都有不同的地址空间,那么每个进程都需要有一个页表,内核通过写 MMU 的 satp 寄存器来切换页表。在切换进程时,内核也需要切换页表。

页表存在于内存中,satp 寄存器里面存放了页表在内存中的物理地址,MMU 通过 satp 寄存器从内存中加载页表项(page table entry, PTE)

在 64 位地址的机器中,有 2^64 个不同的虚拟地址,假如页面大小为 4KB(12 位),则页表索引为高 52 位(64-12),低12 位则为页面偏移量。而页表的作用就是将高位的索引替换为实际物理地址的索引+低位的页内偏移去访问实际的物理内存。

页表项(PTE)有 64 位,其中 10 位保留,44位作为 PPN (physical page number),10 位作为标志位:

是否每个页表项(PTE)都直接映射到对应的物理地址?否,如果直接映射的话,页表会非常大,一般采取混合映射的方式,即直接映射加多级页表。

 Xv6 运行在 Sv39 RISC-V 上,这意味着 xv6 只使用 64 位虚拟地址的低 39 位;未使用高 25 位。也就是说,RISC-V 页表逻辑上是由2^27个页表项(PTEs)组成的数组,每个PTE都包含一个44 位的物理页码(PPN)和一些标志。页硬件通过虚拟地址低 39 位的高 27 位(39 - 12 = 27)来找到对应的页表项,并且得到一个 56 位的物理地址,其顶部 44 位来自页表项中的 PPN,其底部 12 位从原始虚拟地址复制。

注:在上面的单极页表中,虚拟地址的 27 位只是索引,并不在页表项中占用空间,页表项中只包含物理页号和标志位。

在三级页表中,分页硬件使用 27 位中的前 9 位来在根页表页面中选择一个 PTE,中间的 9 位来在树的下一层的页表页面中选择一个 PTE,并使用最下面的 9 位来选择最终的 PTE。如果中间出现页表项不存在的情况,则引发缺页故障(page-fault exception)。

使用多级页表的好处就是可以节省页表所占用的空间,比如一个进程只用到了一个页面,那么除了一个根页表、一个二级页表、一个三级页表外,其他的空间都可以省略,等到需要用到的时候再分配。 缺点就是需要多次访存,如三级页表需要访存三次,取出对应的页表项。为了避免从内存中多次加载页表项,RISCV CPU 使用备用转换缓冲区(Translation Look-aside Buffer (TLB))来存放经常使用的页表项。

每个页表项都有对应的标志位:

  1. PTE_V 指示页表项是否存在,如果没有设置,对页面的引用将导致异常;
  2. PTE_R 控制是否允许将指令读取到该页面;
  3. PTE_W 控制是否允许将指令写入该页面;
  4. PTE_X 控制 CPU 是否可以将页面的内容解释为指令并执行它们;
  5. PTE_U 控制是否允许在用户模式下的指令访问该页面;

这些标志和所有其他与页面硬件相关的结构都在(kernel/riscv.h)中定义。 

2.2 内核地址空间

Xv6 为每个进程维护一个页表,以描述每个进程的用户地址空间;还需要有一个用于描述内核地址空间的页表。内核配置其地址空间的布局,使自己能够在可预测的虚拟地址上访问物理内存和各种硬件资源。文件(kernel/memlayout.h)中声明了 xv6 内核布局中的一些常量,如 KERNBASE,PHYSTOP 等。

内核使用 “直接映射(direct mapping)” 获取 RAM 和内存映射的设备寄存器;即,虚拟地址等于物理地址。直接映射能简化内核读写内存,例如,当 fork 为子进程分配用户内存时,分配器返回该内存的物理地址;当它将父进程的用户内存复制到子进程时,fork 直接将该地址用作虚拟地址。

也有几个内核虚地址是没有直接映射的

  1. The trampoline page:它被映射在虚拟地址空间的顶部;用户页表具有相同的映射。一个物理页面(holding the trampoline code)在内核的虚拟地址空间中映射两次:一次在虚拟地址空间的顶部,一次通过直接映射。
  2. The kernel stack pages:每个进程都有自己的内核栈,再每个进程内核栈的下面会有一个 Guard page,这个页面是无效的(PTE_V is not set),作用是如果内核栈溢出,会直接发生异常,避免栈溢出导致覆盖其他进程的内核栈。

         

虽然内核通过高内存映射使用其堆栈,但内核也可以通过一个直接映射的地址访问它们。但是这种安排中无法通过 Guard page 来提供保护。

2.3 创建地址空间的代码 

大多数用于操作地址空间和页表的 xv6 代码都位于 vm.c(kernel/vm.c)中。

其中核心的数据结构为 pagetable_t,它作为一个指针指向根页表页面,可能是内核页表,也可能是用户进程的页表。

核心的函数为 walk 函数,该函数的作用是找到虚拟地址对应的页表项和为新的映射安装页表项。vm.c 中 kvm 开头的函数操作内核页表,uvm 开头的函数操作用户页表,其他函数则两者都可以。copyout 和 copyin 函数拷贝数据到用户虚拟地址和从用户虚拟地址拷贝数据。

在系统启动时,main 函数会调用 kvminit 函数,kvminit 函数又会调用 kvmmake 函数来创建内核的页表,此时 xv6 并未使能分页机制,所以直接使用物理地址。kvmmake 首先分配一页的物理内存来保存根页表,然后它会调用 kvmmap 来安装内核的指令和数据,以及实际上是设备的内存范围的转换。proc_mapstacks 为每个进程分配一个内核堆栈。它调用 kvmmap 来在 KSTACK 生成的虚拟地址上映射每个堆栈,这为 invalid stack-guard pages 留出了空间。

kvmmap 调用 mappages 来安装一系列虚拟地址到对应物理地址的映射(页表项)到页表中,它以页为间隔对范围内的每个虚拟地址分别执行此操作。

对于每个要映射的虚拟地址,mappages 调用 walk,以查找该地址的 PTE 的地址。然后,它会初始化PTE,以保存相关的物理页码。

walk 使用每个级别的 9 位虚拟地址来查找下一级页表或最后一个页面的 PTE。如果 PTE 无效,则表示尚未分配所需的页面(即未建立该虚拟地址到物理地址的映射);如果设置了 alloc 参数,walk 将分配一个新的页表页面,并将其物理地址放在 PTE 中。它返回树中最低图层中的 PTE 的地址。

上面的代码依赖于物理内存被直接映射到内核虚拟地址空间。例如,当 walk 降低页表的级别时,它从 PTE 中提取下一级页表的(物理)地址,然后使用该地址作为虚拟地址来获取下一级的 PTE。

main 函数调用 kvminithart 来装载内核页表,它会将根页表页面的物理地址写入 satp 寄存器,在这之后 cpu 会使用内核页表来转换地址。

2.4 物理内存分配

内核必须能在运行时分配和释放页表、用户内存、内核栈、和管道 buffer 的物理内存,xv6 使用内核尾部到 PHYSTOP 之间的内存作为运行时分配的内存。每次分配和释放一整个页面(4096B),并且使用链表结构来追踪空闲页面。

这部分代码位于 kernel/kalloc.c 中,结构体 struct run 用于指向可用的页面,kalloc 和 kfree 用于从 freelist 拿取或增添可用的页面,从而实现物理内存的分配与回收。freelist 由一个自旋锁所保护。

main 函数会调用 kinit 来初始化从内核尾部到 PHYSTOP 之间的所有物理页面。分配器有时将地址视为整数,以便对它们进行算术运算(例如,在 freerange 中遍历所有页面),有时使用地址作为读写内存的指针(例如,操作存储在每个页面中的 run 结构)。

2.5 进程地址空间

每个进程都拥有独立的页表,并且当 xv6 切换进程时,页表也要切换。

一个进程的用户内存开始于虚拟地址 0,并且能够增长到 MAXVA(kernel/riscv.h)。并且由程序的 text 段(xv6 使用 PTE_R、PTE_X 和 PTE_U 权限映射)、包含程序预先初始化的数据的页面、栈的页面、堆的页面,Xv6 使用权限 PTE_R、PTE_W 和 PTE_U 映射数据、栈和堆。

通过映射没有 PTE_X 的数据,用户程序不能随意地跳转到程序数据中的一个地址,并在该地址开始执行。

用户栈只有一个页面,并显示了由exec创建的初始内容,包含命令行参数的字符串以及指向它们的指针数组位于堆栈的最顶部。为了检测用户栈溢出,xv6 将堆栈的正下方放置一个不可访问的保护页面(guard page),并且清除 PTE_U 标志。

2.6 sbrk

sbrk 是为进程收缩或增加其内存的系统调用。该系统调用由 growproc 函数实现,growproc 再根据 n 是正数还是负数来使用 uvmalloc 或 uvmdealloc,并且使用 mappages 函数来添加页表项到用户的页表中。

2.7 exec 

exec 为系统调用,它读取一个二进制或可执行文件并替换进程的用户地址空间,exec 首先使用 namei(kernel/exec.c:36)来打开 path 指向的二进制文件,然后读取文件的 ELF header(kernel/elf.h)。

ELF 二进制文件由一个 ELF header、struct elfhdr(kernel/elf.h),紧接着一系列 program section headers struct proghdr(kernel/elf.h)组成。每个 progvhdr 描述了必须加载到内存中的应用程序的一部分;xv6 程序有两个 program section headers:一个用于指令,一个用于数据。

一个 ELF 二进制文件以四字节的 “magic number” 0x7F、“E”、“L”、“F” 开头,即 7f 45 4c 46。exec 会先使用 readi 函数从该文件的 inode 中读出 elfhdr,然后查看 elfhdr 中的 magic 是否和 ELF_MAGIC 一致;然后使用 proc_pagetable 分配一个用户页表给当前进程,但并未进行用户内存空间映射,只对用户空间顶部的 trampoline code 进行了映射。然后读取后续的 proghdr,使用 uvmalloc 给每个段分配页面,最后使用 loadseg 来导入每个段到内存中。

exec 的后续代码会先分配两个页面,第一个页面作为 guard page,第二个页面作为用户栈,然后将 argv 中的参数先存储到栈中,再将参数的地址数组 ustack 存到栈中,再将 ustack 的地址存储到 p->trapframe->a1,return argc 表示将 argc 存放到 p->trapframe->a0(函数的返回值会存放到 a0 寄存器)。最后再保存进程的映像,如进程的新页表,进程的入口,栈指针等。

2.8 C 语言和汇编如何相互调用?

调用约定(Calling Convention)。

Base ISA: Program counter, 32 general-purpose registers (x0--x31)

  • ra 寄存器需要调用者保存。
  • sp 寄存器需要被调用者保存。
  • t0-t6 临时寄存器需要调用者保存。
  • s0-s11 保存寄存器需要被调用者保存。
  • a0-a7 函数参数寄存器需要调用者保存。

在 rv32 和 rv64 中,int 类型都是 32 位,而 long 和 指针类型在 rv32 中是 32 位,在 rv64 则是 64 位。

三、Lab: page tables

3.1 Speed up system calls (easy)

一些系统调用将数据放到用户空间和内核空间之间的只读区域(即内核和用户都可以访问,但是用户只能读页面),从而绕过内核实现加速。而本实验就是要在 trapframe 下定义一个新的用户只能读的页放在 USYSCALL(USYSCALL 是一个虚拟地址,在 memlayout.h 中定义):

在该页的起始位置存放一个 struct usyscall 结构体(kernel/memlayout.h),里面存放当前进程的 pid, 因此可以提供一个 ugetpid() 函数给用户,该函数直接在用户态从该结构体中读出 pid,省去了 getpid() 系统调用切换到内核态的时间:

实现:

首先在 kernel/proc.h 中的 proc 结构体中定义结构体 usyscall 的指针 usyscall_page:

// 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_page;
  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)
};

然后在 kernel/proc.c 的 allocproc 函数中为 usyscall_page 分配物理页面,并且为 usyscall 结构体的 pid 赋值:

// 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.
static struct proc*
allocproc(void)
{
  struct proc *p;

  for(p = proc; p < &proc[NPROC]; p++) {
    acquire(&p->lock);
    if(p->state == UNUSED) {
      goto found;
    } else {
      release(&p->lock);
    }
  }
  return 0;

found:
  p->pid = allocpid();
  p->state = USED;

  // Allocate a trapframe page.
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // Allocate a usyscall page
  if((p->usyscall_page = (struct usyscall *)kalloc()) == 0)
  {
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  p->usyscall_page->pid = p->pid;

  // An empty user page table.
  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.
  memset(&p->context, 0, sizeof(p->context));
  p->context.ra = (uint64)forkret;
  p->context.sp = p->kstack + PGSIZE;

  return p;
}

相应地,在 freeproc() 函数中需要释放该物理页面(不释放的话后面的 usertest 通过不了):

// free a proc structure and the data hanging from it,
// including user pages.
// p->lock must be held.
static void
freeproc(struct proc *p)
{
  if(p->trapframe)
    kfree((void*)p->trapframe);
  if(p->usyscall_page)
    kfree((void*)p->usyscall_page);
  p->trapframe = 0;
  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;
}

在 kernel/proc.c 中的 proc_pagetable() 函数中使用 mappages() 函数创建虚拟地址 USYSCALL 到物理地址的映射,并将其作为页表项(PTE)存入当前进程的页表中,该页的权限位为 PTE_R | PTE_U,即只读,并且用户可以访问:

// Create a user page table for a given process, with no user memory,
// but with trampoline and trapframe pages.
pagetable_t
proc_pagetable(struct proc *p)
{
  pagetable_t pagetable;

  // An empty page table.
  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.
  if(mappages(pagetable, TRAMPOLINE, PGSIZE,
              (uint64)trampoline, PTE_R | PTE_X) < 0){
    uvmfree(pagetable, 0);
    return 0;
  }

  // map the trapframe page just below the trampoline page, for
  // trampoline.S.
  if(mappages(pagetable, TRAPFRAME, PGSIZE,
              (uint64)(p->trapframe), PTE_R | PTE_W) < 0){
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }

  // 映射 USYSCALL 页面 below the trapframe page,read only
  if(mappages(pagetable, USYSCALL, PGSIZE,
              (uint64)(p->usyscall_page), PTE_R | PTE_U) < 0){
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmunmap(pagetable, TRAPFRAME, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }

  return pagetable;
}

相应的,在 proc_freepagetable 也要解除该页的映射:

// Free a process's page table, and free the
// physical memory it refers to.
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
  uvmunmap(pagetable, TRAMPOLINE, 1, 0);
  uvmunmap(pagetable, TRAPFRAME, 1, 0);
  uvmunmap(pagetable, USYSCALL, 1, 0);
  uvmfree(pagetable, sz);
}

执行 make qemu,在 xv6 中运行 pgtbltest 会显示:

ugetpid_test starting
ugetpid_test: OK

3.2 Print a page table (easy)

此部分需要实现一个函数 vmprint,此函数能够递归打印涉及到的页表项,如下图所示:

这是打印 pid = 1 的进程的页表,首先打印页表地址,然后递归打印页表项和对应的物理地址。

代码实现如下,首先在 vm.c 中添加 vmprint() 函数,可以参考同文件下的 freewalk 函数,此函数作用为递归地释放非叶节点页表的空间: 

static void
vmprintpte(pagetable_t pagetable, int depth)
{
        for(int i = 0; i < 512; i++)
        {
                pte_t pte = *(pagetable + i);
                if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0)
                {
                        for(int j = 0; j < depth; j++)
                                printf("..");
                        printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));
                        uint64 child = PTE2PA(pte);
                        vmprintpte((pagetable_t)child, depth+1);
                }
                else if(pte & PTE_V)
                {
                        // leaf page
                        for(int j = 0; j < depth; j++)
                                printf("..");
                        printf("%d: pte %p pa %p\n", i, pte, PTE2PA(pte));
                }
        }
}

// vmprint, Recursively print page-table.
void
vmprint(pagetable_t pagetable)
{
        printf("page table %p\n", pagetable);
        vmprintpte(pagetable, 1);
}

然后到 kernel/defs.h 中增加 vmprint() 函数的定义:

void            vmprint(pagetable_t);

最后到 kernel/exec.c 的 exec 函数的 return argc 前加入如下代码,即如果进程为初始进程,则递归打印其页表:

// vmprint
if(p->pid == 1)
{
      vmprint(p->pagetable);
}

使用 make qemu 启动 xv6 即可看到打印结果: 

3.3 Detect which pages have been accessed (hard) 

此部分需要我们实现 pgaccess() 函数,该函数用来确认传入的页面是否被访问过。这个函数需要三个参数,第一个参数为用户传入的页面的起始虚拟地址;第二个参数为需要检查的页数;第三个参数为一个地址,用来返回结果,将结果存储到位中(每页使用一位,其中第一页对应于最低有效位)。

首先我们需要定义 PTE_A,即访问位,查阅 RISC-V privileged architecture manual 手册可知,PTE_V 为第六位:

然后在 kernel/riscv.h 中定义 PTE_V:

#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) // user can access

#define PTE_A (1L << 6) // access bit

pgaccess() 函数是一个系统调用,其对应的内核函数为 sys_pgaccess() (实现在 kernel/sysproc.c 中),代码如下:

int
sys_pgaccess(void)
{
  // lab pgtbl: your code here.
  uint64 base;
  int len;
  uint64 mask;
  unsigned int abits = 0;
  pte_t *pte;
  struct proc *p = myproc();
  argaddr(0, &base);
  argint(1, &len);
  argaddr(2, &mask);

  //vmprint(p->pagetable);
  //printf("%p\n", PTE2PA(*walk(p->pagetable, base, 0)));

  for (int i = 0; i < len && base < MAXVA; i++, base += PGSIZE)
  {
    if ((pte = walk(p->pagetable, base, 0)) != 0 && (*pte & PTE_A))
    {
      abits |= (1 << i);
      *pte &= ~PTE_A;
    }
  }

  if (copyout(p->pagetable, mask, (char *)&abits, sizeof(abits)) < 0)
  {
    return -1;
  }
  //printf("aa%d\n", abits);
  return 0;
}

此时执行 pgtbltest 可以看到测试用例全部通过: 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Bert4keras相关环境安装

1. 创建虚拟环境&#xff0c;先安装cudatoolkit和cudnn cudatoolkit版本不对容易报so库找不到的错。 具体安装参考&#xff1a;在conda虚拟环境中安装cuda和cudnn&#xff0c;解决pip install or python setup.py install 将安装包安装到全局环境中的问题&#xff08;1.2部分&…

有源功率因数校正电路的设计(论文+仿真)

1. 系统设计 本文围绕有源功率因数校正电路的设计而展开&#xff0c;拟以BOOST的拓扑结构进行有源功率因数校正电路的搭建&#xff0c;通过不同电流模式的Boost变换电路实际应用及其分析&#xff0c;能够总结最优的解决设计方案&#xff0c;在方案建立的基础之上&#xff0c;通…

企业数字档案馆室建设指南

数字化时代&#xff0c;企业数字化转型已经成为当下各行业发展的必然趋势。企业数字化转型不仅仅是IT系统的升级&#xff0c;也包括企业内部各种文件、档案、合同等信息的数字化管理。因此&#xff0c;建设数字档案馆室也变得尤为重要。本篇文章将为您介绍企业数字档案馆室建设…

华为鸿蒙爆发真实力!原生应用媲美iOS,使用流畅度将提升20至30%

随着华为鸿蒙原生应用开发计划的启动&#xff0c;一场席卷全球的科技浪潮正在涌动。鸿蒙生态的快速发展&#xff0c;吸引了无数企业和开发者的关注&#xff0c;他们纷纷拥抱这个新兴的生态系统&#xff0c;共同构建一个更加繁荣的鸿蒙世界。 华为鸿蒙原生应用开发计划引爆全球…

【计算机二级MS Office】word(上)

这里写目录标题 文件选项卡保存和另存为属性检查文档 开始选项卡字体更改字体和字号设置中文和英文为两种不同字体的快捷方式介绍其余图标文本效果突出颜色如何挑选字体颜色字符底纹带圈字符字体对话框&#xff08;隐藏&#xff09; 段落 插入选项卡设计选项卡布局选项卡引用选…

Ribbon 饥饿加载

Ribbon默认是采用懒加载&#xff0c;即第一次访问时才会去创建LoadBalanceClient&#xff0c;请求时间会很长而饥饿加载则会在项目启动时创建&#xff0c;降低第一次访问的耗时&#xff0c;通过下面配置开启饥饿加载: 一、懒加载 Ribbon 默认为懒加载即在首次启动Application…

妈妈和老婆掉进水里,孟子认为先救老婆是禽兽

妈妈和老婆同时掉进水里先救谁&#xff1f; 多数男人不敢回答&#xff0c;怕得罪老婆。孟子认为&#xff0c;必须先救妈妈。因为父母都照顾不好&#xff0c;是不孝。先救妈妈&#xff0c;再救老婆。 中国人的信仰是天&#xff0c;以及孔孟之道。中国人不信宗教。 孟子&#x…

mysql锁介绍

锁分类&#xff1a; 加锁时机 普通select 语句会加MDL读锁&#xff0c;防止DDL的MDL写锁产生冲突 update、insert、delete会自动加入排它锁X&#xff0c;以及意向IX锁 select ... lock in share mode 加意向IS锁以及共享S锁 select... for update 加意向IX锁以及排它锁X

数据结构(超详细讲解!!)第二十六节 图(下)

1.无向图的连通分量 图遍历时&#xff0c;对于连通图&#xff0c;无论是广度优先搜索还是深度优先搜索&#xff0c;仅需要调用一次搜索过程&#xff0c;即从任一个顶点出发&#xff0c;便可以遍历图中的各个顶点。对于非连通图&#xff0c;则需要多次调用搜索过程&#xff0c;而…

二叉平衡树

一直想深入的研究一下&#xff0c;并手写平衡二叉树的插入、删除代码。 二叉树是动态查找的典范&#xff0c;但在极限情况下&#xff0c;二叉树的查找效果等同于链表&#xff0c;而平衡二叉树可以完美的达到 log ⁡ 2 n \log_2 n log2​n。 AVL简称平衡二叉树&#xff0c;缩写…

[Mac软件]HitPaw Photo Object Remover Mac v1.2.1 Ai智能去水印工具图像物体移除

多亏了HitPaw Photo Object Remover&#xff0c;它会自动跟踪和识别对象&#xff0c;并帮助您通过几个简单的步骤从照片中删除您想要的所有内容。 使用人工智能删除照片中物体的一流工具 •像1-2-3一样轻松地从照片中删除对象 •人工智能技术有助于立即清除不必要的物体 •轻…

激光雷达生成的图像检测关键点用来辅助里程计的方案

文章&#xff1a;LiDAR-Generated Images Derived Keypoints Assisted Point Cloud Registration Scheme in Odometry Estimation 作者&#xff1a;Haizhou Zhang , Xianjia Yu, Sier Ha , Tomi Westerlund 编辑&#xff1a;点云PCL 欢迎各位加入知识星球&#xff0c;获取PDF…

springboot 整合 Spring Security+JWT 实现token 认证和校验

1.大概是这个样子 JWT 是什么&#xff1f; Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准&#xff08;(RFC 7519).该token被设计为紧凑且安全的&#xff0c;特别适用于分布式站点的单点登录&#xff08;SSO&#xff09;场景。JWT的声明…

有源滤波器在矿区配电网中的应用

针对目前有源滤波器应用于矿区谐波治理时电网频率适应能力较低的问题&#xff0c;针对定采样点数字控制系统提出了一种具有频率自适应能力的谐振控制策略。该策略不仅可以实现对电网频率波动的自适应&#xff0c;提高滤波器补偿效果&#xff0c;而且不需要在线对控制器参数进行…

Apipost版IDEA插件

Apipost-Helper是由Apipost推出的IDEA插件&#xff0c;写完接口可以进行快速调试&#xff0c;且支持搜索接口、根据method跳转接口&#xff0c;还支持生成标准的API文档&#xff0c;注意&#xff1a;这些操作都可以在代码编辑器内独立完成&#xff0c;非常好用&#xff01;这里…

设计原则 | 依赖转置原则

一、依赖转置原则&#xff08;DIP&#xff1a;Dependence Inversion Principle&#xff09; 1、原理 高层模块不应该依赖低层模块&#xff0c;二者都应该依赖于抽象抽象不应该依赖于细节&#xff0c;细节应该依赖于抽象 2、层次化 Booch曾经说过&#xff1a;所有结构良好的面…

丢包问题定位(一)

1,原理图 从上图我们分两个部分,一部分是host测(靠近交换sdk测) ,一部分是line测(靠近光模块测) 这里我们靠近交换(与交换测连接的是retimer芯片)。 当设备丢包时我们怎么定位呢! 先弄清楚设备的连接情况,确认是模块问题,设备问题,还是参数问题,前两种是个例,…

利用udev 修改 网卡名称 的方法和规则文件不生效 可能的查找方法

为什么要修改? 服务器通常有多块网卡,有板载集成的,同时也有插在PCIe插槽的。Linux系统的命名原来是eth0,eth1这样的形式,但是这个编号往往不一定准确对应网卡接口的物理顺序。我们也希望能跟设备外部的丝印对的上 方法: 利用udev 机制。 在 /etc/udev/rules.d/ 下增加…

指针(二)

这里写目录标题 字符指针字符指针与常量字符串的区别&#xff1a; 指针数组数组指针两者的区别&#xff1a;&数组名 &#xff0c;sizeof(arr)数组指针的使用数组参数&#xff0c;指针参数一维数组传参整型数组&#xff1a;整型指针数组&#xff1a; 一级指针传参二级指针传…

配置REST API数据访问

大家好&#xff0c;才是真的好。 我希望你看过前一篇内容《Domino REST API安装和运行》。没有看过也没有关系&#xff0c;毕竟你可以点击上面的链接再去看一遍。 至于这一篇&#xff0c;趁热打铁&#xff0c;我们主要来讲述如何配置Domino Rest API的数据访问。 首先这里有…