MIT 6s081 lab3:page tables

news2025/1/11 10:03:11

Lab3: page tables

作业地址:Lab: page tables (mit.edu)

本实验的目标:修改页表、简化从用户态拷贝数据到内核态的方法

其实页表就几个操作:创建页表、添加PTE项,查找PTE项,清空PTE项,释放PTE对应的物理空间,释放页表本身占用的物理空间

Print a page table (easy)

添加一个打印页表的内核函数

page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000

添加下列函数,并在/kernel/defs.h中声明

int             vmprint(pagetable_t pagetable); 

代码如下:通过迭代的方式遍历三级页表,PTE2PA函数用于获取页表条目PTE对应的物理地址PA

void vmprint(pagetable_t pagetable)
{
    // there are 2^9 = 512 PTEs in a page table.(一级页表)
    printf("page table %p\n", pagetable);
    for(int i = 0; i < 512; i++) { // 遍历一级页表
        pte_t pte = pagetable[i]; // 对于每一项,如果valid,并且RWX不全为1,说明这一条目有效
        if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0) {
          // this PTE points to a lower-level page table.
          uint64 pa_2 = PTE2PA(pte); // 得到二级页表
          printf("..%d: pte %p pa %p\n",i, pte, pa_2);
          
          // 遍历二级页表 
          for(int j = 0; j < 512; j++) {
              pte_t pte_2 = ((pagetable_t)pa_2)[j];
              if((pte_2 & PTE_V) && (pte_2 & (PTE_R|PTE_W|PTE_X)) == 0) {
                  uint64 pa_3 = PTE2PA(pte_2); // 得到三级页表
                  printf(".. ..%d: pte %p pa %p\n", j, pte_2, pa_3);
                  
                  // 遍历三级页表
                  for(int k = 0; k < 512; k++) {
                      pte_t pte_3 = ((pagetable_t)pa_3)[k];
                      if((pte_3 & PTE_V)) {
                        // printf("debug\n");
                          uint64 pa = PTE2PA(pte_3);
                          printf(".. .. ..%d: pte %p pa %p\n", k, pte_3, pa);
                      }
                  }
              }
          }
        }
    }

}

A kernel page table per process (hard)

xv6 原本的设计是,用户进程在用户态使用各自的用户态页表,但是一旦进入内核态(例如使用了系统调用),则切换到内核页表(通过修改 satp 寄存器,trampoline.S)。然而这个内核页表是全局共享的,也就是全部进程进入内核态都共用同一个内核态页表:

本 Lab 目标是让每一个进程进入内核态后,都能有自己的独立内核页表,为第三个实验做准备。也就是要把内核页表的内容拷贝一份给进程的内核页表副本,同时还拥有进程各自的内核栈,在不同的进程运行时,使用进程的内核页表副本,没有进程运行时,使用全局的内核页表。

创建进程的内核页表和内核栈

1、在进程的结构体proc.h中添加一个字段:

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

  // p->lock must be held when using these:
  enum procstate state;        // Process state
  struct proc *parent;         // Parent process
  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

  // 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 context context;      // swtch() here to run process
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)
  // lab3 add
  pagetable_t ptb_k;           // kernel page table
};

2、 定义一个函数,用于在初始化进程的时候,初始化一个内核页表的副本(仿照/kernel/main.c中的kvminit函数为页表添加一些固定的映射如UART、CLINE、KERNBASE等,并仿照kvmmap函数,添加一个ptb_kvmmap函数,添加一个参数pagetable_t);

extern char etext[];  // kernel.ld sets this to end of kernel code.
pagetable_t init_ptb_k()
{
  pagetable_t ptb_k = (pagetable_t) kalloc();
  if(ptb_k == 0) return 0;

  memset(ptb_k, 0, PGSIZE);

  // 使用自定义的kvmmap函数进行一些必须有的内核页表项(UART0、VIRTIO0等)的映射
  ptb_kvmmap(ptb_k, UART0, UART0, PGSIZE, PTE_R | PTE_W);
  ptb_kvmmap(ptb_k, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
  ptb_kvmmap(ptb_k, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
  ptb_kvmmap(ptb_k, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
  ptb_kvmmap(ptb_k, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
  ptb_kvmmap(ptb_k, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
  ptb_kvmmap(ptb_k, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);

  return ptb_k;
}

// 用于进程的内核页表副本的vmmap,添加了参数pagetable_t ptb_k
void ptb_kvmmap(pagetable_t ptb_k, uint64 va, uint64 pa, uint64 sz, int perm)
{
  if(mappages(ptb_k, va, sz, pa, perm) != 0)
    panic("kvmmap");
}

在allocproc函数中,调用init_ptb_k函数。在原本的使用内核页表的程序中,在kernel/main.c中procinit.c中,在内核的启动时期对所有进程的内核栈都进行了分配和建立映射

在这里把这部分进行删去,在各自进程的内核页表副本中进行创建。

// initialize the proc table at boot time.
void
procinit(void)
{
  struct proc *p;
  
  initlock(&pid_lock, "nextpid");
  for(p = proc; p < &proc[NPROC]; p++) {
      initlock(&p->lock, "proc");

      // Allocate a page for the process's kernel stack.
      // Map it high in memory, followed by an invalid
      // guard page.

      // (lab3)move to allocproc
      // char *pa = kalloc();
      // if(pa == 0)
      //   panic("kalloc");
      // uint64 va = KSTACK((int) (p - proc));
      // kvmmap(va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
      // p->kstack = va;
  }
  kvminithart();
}

在allocproc中初始化内核页表副本和分配、映射进程对应的内核栈

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();

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

  // An empty user page table.
  p->pagetable = proc_pagetable(p); 
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
	
  // lab3 add begin
  // 在main.c/procinit()中对所有的64个进程预先分配了一个内核栈,用于用户进程陷入内核态后使用的栈
  p->ptb_k = init_ptb_k();
  // 1、删除procinit函数中对所有进程内核栈的分配和映射
  // 2、在各自的进程中对内核栈进行分配
  char *pa = kalloc();
  if(pa == 0)
    panic("kalloc");
  uint64 va = KSTACK((int) (p - proc));
  ptb_kvmmap(p->ptb_k, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
  p->kstack = va; 
  
  // lab3 add end
  
  // 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;
}

切换到进程内核页表

scheduler函数用于对于cpu c,遍历全局的进程数组,找到第一个状态为RUNNABLE的进程,并切换到这个进程。

因此,仿照kvminithart()函数,在上下文切换之前,加载进程p提供的内核页表副本,并在返回时及时恢复全局的内核页表

void
scheduler(void)
{
  struct proc *p;
  struct cpu *c = mycpu();
  
  c->proc = 0;
  for(;;){
    // Avoid deadlock by ensuring that devices can interrupt.
    intr_on();
    
    int found = 0;
    for(p = proc; p < &proc[NPROC]; p++) {
      acquire(&p->lock);
      if(p->state == RUNNABLE) {
        // Switch to chosen process.  It is the process's job
        // to release its lock and then reacquire it
        // before jumping back to us.
        p->state = RUNNING;
        c->proc = p;
		
		//lab3 add begin
        // 切换成进程的内核页表副本
        if(p->ptb_k) {
            w_satp(MAKE_SATP(p->ptb_k));
            sfence_vma();
        }
         
        swtch(&c->context, &p->context); //通过swtch.S中的汇编程序进行cpu的上下文切换,在lab7中提及

        kvminithart(); // 需要及时恢复内核的页表
        // lab3 add end
        
        // Process is done running for now.
        // It should have changed its p->state before coming back.
        c->proc = 0;

        found = 1;
      }
      release(&p->lock);
    }
#if !defined (LAB_FS)
    if(found == 0) {
      // kvminithart(); // 恢复内核的页表
      intr_on();

      asm volatile("wfi");
    }
#else
    ;
#endif
  }
}

释放进程内核页表和内核栈

在进程销毁之前,需要释放进程中使用的空间,需要对之前创建的内核页表副本和内核栈进行释放。

注意:对于内核页表副本,只能释放页表本身占用的物理空间,不能释放页表的叶子节点对应的物理页(这会导致内核运行所需要的关键物理页被释放,导致内核崩溃。)

static void
freeproc(struct proc *p)
{
  
  if(p->trapframe)
    kfree((void*)p->trapframe);
  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;
  

  // lab3 add begin
  // 释放进程的内核栈
  void * pa = (void *)kvmpa2(p->ptb_k, p->kstack); // 使用kvmpa2函数,传入页表和虚拟地址,返回物理地址
  kfree(pa);
  p->kstack = 0;

  // 释放进程对应的三级页表,但不释放物理页
  freewalk2(p->ptb_k);
  p->ptb_k = 0;

  p->state = UNUSED;
}

uint64 kvmpa2(pagetable_t ptb_k,uint64 va)
{
  uint64 off = va % PGSIZE;
  pte_t *pte;
  uint64 pa;

  pte = walk(ptb_k, va, 0);
  if(pte == 0)
    panic("kvmpa");
  if((*pte & PTE_V) == 0)
    panic("kvmpa");
  pa = PTE2PA(*pte);
  return pa+off;
}

void freewalk2(pagetable_t pagetable) // 递归释放内核页表的所有mapping,但是不释放对应的物理页
{
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++) {
    pte_t pte = pagetable[i];
    if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){ // 对于最高级和中间级,PTE_V为1,但PTE_R|W|X都为0
      // this PTE points to a lower-level page table.
      uint64 child = PTE2PA(pte); 
      freewalk2((pagetable_t)child);
      pagetable[i] = 0;
    }
    else if(pte & PTE_V){
      pagetable[i] = 0; //清空最后一级的页表项
    }
  }
  kfree((void*)pagetable); //释放每一级的页表占用的物理空间
}

//lab3 add end

调试

xv6 kernel is booting

hart 2 starting
hart 1 starting
panic: kvmpa

陷入了kvmpa

查询调用kvmpa的函数:发现只有

kernel/virtio_disk.c

注意到在virtio磁盘驱动virtio_disk.c中调用了kvmpa()将虚拟地址转换为物理地址,这个函数需要改为kvmpa2,因此此时使用的页表是进程的内核页表副本。

disk.desc[idx[0]].addr = (uint64) kvmpa2(myproc()->ptb_k ,(uint64) &buf0); // 这里不能用内核的页表了,因为已经切换成进程的页表了

调用usertests,结果:

test opentest: OK
test writetest: OK
test writebig: OK
test createtest: OK
test openiput: OK
test exitiput: OK
test iput: OK
test mem: OK
test pipe1: OK
test preempt: kill... wait... OK
test exitwait: OK
test rmdot: OK
test fourteen: OK
test bigfile: OK
test dirfile: OK
test iref: OK
test forktest: OK
test bigdir: OK
ALL TESTS PASSED

Simplify copyin/copyinstr (hard)

一开始没看懂是啥意思,英语太差了。。。找了好多博客才看明白要干啥。指导书的提示也很清晰,基本坑的点都提到了。但是容易写错,debug也很麻烦。实在搞不出来就看了答案,要保证思路是大致差不多的。也就是把进程的页表拷贝到进程的内核页表副本。

替换copyin内部的实现为对copyin_new的调用,替换copyinstr内部的实现为对copyinstr_new的调用。

在每个进程的内核页表副本中添加用户地址的映射,从而copyin和copyinstr能够正常工作。

在这里插入图片描述

在这里插入图片描述

因为进程的虚拟地址空间是从0开始的,根据指导书的提升,把进程的这部分空间移到内核虚拟地址空间PLIC下面这段空间,CLINT这段空间只有在boot过程会使用,后续不需要使用。

因此在kvminit()函数中保留对CLINT的映射,在用户进程创建的内核页表副本中去除对CLINT的映射。

所以这个实验要做的就是把进程的虚拟地址空间对应的页表 拷贝到前面那个实验为每个进程创建的内核页表副本,从而提高copyin的速度(原来copyin是先使用进程的页表把虚拟地址空间翻译成物理地址空间,然后再进行访问。)

也就是要在所有对进程的页表进行修改的地方,都要同步修改操作到进程的内核页表副本里面。

[mit6.s081] 笔记 Lab3: Page tables | 页表 | Miigon’s blog

在这里插入图片描述

根据指导书:需要同步的地方有:fork、exec、sbrk函数,以及在kernel/main.c中创建的第一个用户进程。

创建工具方法(copy、缩小)

首先为页表的copy和缩小编写工具方法:

  • kvmcopymappings用于将 从oldstart开始的sz字节的PTE拷贝到new
  • kvmdealloc用于将PTE从oldsz缩减到newsz,但不释放实际内存
// 参考int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz),实现进程的页表拷贝到内核的页表,但不拷贝对应的物理页内存
int kvmcopymappings(pagetable_t src, pagetable_t dst, uint64 start, uint64 sz)
{
  pte_t *pte;
  uint64 pa, i;
  uint flags;
  // prevent re-mapping already mapped pages (eg. when doing growproc)
  for(i = PGROUNDUP(start); i < start + sz; i += PGSIZE){
    if((pte = walk(src, i, 0)) == 0)
      panic("kvmcopymappings: pte should exist");
    if((*pte & PTE_V) == 0)
      panic("kvmcopymappings: page not present");
    pa = PTE2PA(*pte);

    flags = PTE_FLAGS(*pte) & (~PTE_U); //清空PTE_U flag,这里要注意,指导书有提示,要清除这个标志。内核才能访问这个页

    if(mappages(dst, i, PGSIZE, pa, flags) != 0){
      goto err;
    }
  }
  return 0;

 err:
  uvmunmap(dst, PGROUNDUP(start), (i-PGROUNDUP(start)) / PGSIZE, 0); //把前面的页全部释放掉(PTE),但不释放物理页
  return -1;
}


// 参考uvmdealloc,将程序内存从oldsz缩减到newsz,但不释放实际内存,用于同步用户页表程序内存映射与内核页表程序内存映射的同步
uint64 kvmdealloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz)
{
  if(newsz >= oldsz)
    return oldsz;

  if(PGROUNDUP(newsz) < PGROUNDUP(oldsz)){
    int npages = (PGROUNDUP(oldsz) - PGROUNDUP(newsz)) / PGSIZE;
    uvmunmap(pagetable, PGROUNDUP(newsz), npages, 0);
  }

  return newsz;
}

fork

修改fork函数,调用kvmcopymappings进行拷贝

// Copy user memory from parent to child.
  // 添加将用户页表拷贝到内核页表
  if((uvmcopy(p->pagetable, np->pagetable, p->sz) < 0) || (kvmcopymappings(np->pagetable, np->ptb_k, 0, p->sz) < 0))
  {
    freeproc(np);
    release(&np->lock);
    return -1;
  }

exec

添加检查,防止程序内存超过PLIC

 if(ph.vaddr + ph.memsz < ph.vaddr)
      goto bad;
    uint64 sz1;
    if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz)) == 0)
      goto bad;
    if(sz1 >= PLIC) { // 添加检测,防止程序大小超过 PLIC
      goto bad; 
    }
    sz = sz1;
    if(ph.vaddr % PGSIZE != 0)
      goto bad;
    if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
      goto bad;

清除原来内核页表对程序内存的映射,然后重建建立映射

// Save program name for debugging.
  for(last=s=path; *s; s++)
    if(*s == '/')
      last = s+1;
  safestrcpy(p->name, last, sizeof(p->name));

  //清除内核页表中对程序内存的旧映射,重新建立映射
  uvmunmap(p->ptb_k, 0, PGROUNDUP(oldsz)/PGSIZE, 0);
  kvmcopymappings(pagetable, p->ptb_k, 0, sz);
    
  // Commit to the user image.
  oldpagetable = p->pagetable;
  p->pagetable = pagetable;
  p->sz = sz;
  p->trapframe->epc = elf.entry;  // initial program counter = main
  p->trapframe->sp = sp; // initial stack pointer
  proc_freepagetable(oldpagetable, oldsz);

sbrk/growproc

sbrk实际调用的是growproc,对扩大和缩小都进行同步

int
growproc(int n)
{
  uint sz;
  struct proc *p = myproc();

  sz = p->sz;
  if(n > 0){
    uint64 newsz;
    if((newsz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
      return -1;
    }
    // 内核中的页表的映射同步扩大
    if(sz + n > PLIC) return -1; 
    if(kvmcopymappings(p->pagetable, p->ptb_k, sz, n) != 0){
      uvmdealloc(p->pagetable, newsz, sz);
      return -1;
    }
    sz = newsz;
  } else if(n < 0){
    uvmdealloc(p->pagetable, sz, sz + n);

    // 内核页表中的映射同步缩小
    sz = kvmdealloc(p->ptb_k, sz, sz + n); //释放sz 到sz+n 的映射,但不释放对应的物理页,在uvmdealloc(p->pagetable,sz,sz + n)已经删过了
  }
  p->sz = sz;
  return 0;

userinit

void
userinit(void)
{
  struct proc *p;

  p = allocproc();
  initproc = p;
  
  // allocate one user page and copy init's instructions
  // and data into it.
  uvminit(p->pagetable, initcode, sizeof(initcode));
  p->sz = PGSIZE;

  //对于init进程,第一个用户进程,添加同步映射
  kvmcopymappings(p->pagetable, p->ptb_k, 0, p->sz);
  // prepare for the very first "return" from kernel to user.
  p->trapframe->epc = 0;      // user program counter
  p->trapframe->sp = PGSIZE;  // user stack pointer

  safestrcpy(p->name, "initcode", sizeof(p->name));
  p->cwd = namei("/");

  p->state = RUNNABLE;
  release(&p->lock);
}

提交

$ make grade
$ make qemu-gdb
pte printout: OK (2.8s) 
== Test answers-pgtbl.txt == answers-pgtbl.txt: FAIL 
    Cannot read answers-pgtbl.txt
== Test count copyin == 
$ make qemu-gdb
count copyin: OK (0.6s) 
== Test usertests == 
$ make qemu-gdb
(151.9s) 
== Test   usertests: copyin == 
  usertests: copyin: OK 
== Test   usertests: copyinstr1 == 
  usertests: copyinstr1: OK 
== Test   usertests: copyinstr2 == 
  usertests: copyinstr2: OK 
== Test   usertests: copyinstr3 == 
  usertests: copyinstr3: OK 
== Test   usertests: sbrkmuch == 
  usertests: sbrkmuch: OK 
== Test   usertests: all tests == 
  usertests: all tests: OK 
== Test time == 
time: OK 
Score: 61/66
make: *** [Makefile:316: grade] Error 1

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

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

相关文章

聚观早报 |一加Ace 3鸣沙金开售;地平线征程6将发布

聚观早报每日整理最值得关注的行业重点事件&#xff0c;帮助大家及时了解最新行业动态&#xff0c;每日读报&#xff0c;就读聚观365资讯简报。 整理丨Cutie 1月16日消息 一加Ace 3鸣沙金开售 地平线征程6将发布 互动短剧《谍影成双》首曝PV 台积电发布四季度财报 保时捷…

JVM实战(21)——jstat实战(2)

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 学习必须往深处挖&…

Proxy的使用方法和13种拦截操作

前言 proxy是ES6新推出的方法,功能很强大。属于元编程,也就是修改js本身的一些东西。可以对数组,对象,函数等引用类型的对象进行一些复杂的操作。 其中,大部分人应该最熟悉的莫过于vue3中使用proxy替换了defineProperty,而且还实现了本身defineProperty不能实现的一些东西。 …

Vue-API

$parent 和 $children $parent 父传子--在子组件中使用&#xff0c;放在计算属性、生命周期中&#xff1a; $children 子传父--方法中使用&#xff1a; $nextTick: $ref: 操作dom $set、$delete:

UML-实现图(组件图和部署图)

实现图是从系统的层次来描述的&#xff0c;描述硬件的组成和布局&#xff0c;描述软件系统划分和功能实现。 UML-实现图&#xff08;组件图和部署图&#xff09; 一、组件图1.组件图的元素&#xff08;1&#xff09;组件&#xff08;2&#xff09;接口&#xff08;3&#xff09…

linux系统nginx工具的一些应用和基于nginx做虚拟主机

nginx高级应用 虚拟目录监控模块配置文件创建用户名密码客户端访问 限制传输速度&#xff08;服务层&#xff09; nginx虚拟主机基于ip基于域名基于端口 nginx配置文件中的每个语句要以 ; 结尾 虚拟目录 配置文件中的server块中编辑&#xff1a;location /test {alias /usr/…

SparkSQL初体验

SparkSQL初体验 命令式的 API RDD 版本的 WordCount val conf new SparkConf().setAppName("ip_ana").setMaster("local[6]") val sc new SparkContext(conf)sc.textFile("hdfs://master:9000/dataset/wordcount.txt").flatMap(_.split("…

Visual Studio 2019 ctrl+f 呼出查找和替换窗口

有时候 ctrlshiftf 呼出查找和替换窗口不起作用&#xff0c;可能和其它程序的快捷键冲突&#xff0c;解决方案&#xff1a; ------------英文版本------------ 依次点击VS菜单栏中的 Tools - Options - Environment - Keyboard: 1. 在右侧的 Show commands containing: 文本框输…

单列的堆叠柱状图

目的 MSingleColumnStackBarChart类被设计用于创建只有单列的堆叠柱状图&#xff0c;用于血糖数据的统计。以下是封装这个类的目的的详细描述&#xff1a; 抽象复杂性&#xff1a; 通过创建MSingleColumnStackBarChart类&#xff0c;你将复杂的MPAndroidChart库的使用和配置封…

创意交融:集成自定义报表和仪表盘设计器,实现图标替换

前言 在现代数据分析领域&#xff0c;随着对报表和数据分析的需求不断增长&#xff0c;市场上涌现了许多嵌入式报表工具。这些工具能够与企业现有的OA、ERP、MES、CRM等应用系统深度集成&#xff0c;实现对业务数据的自助式分析。然而&#xff0c;在实际应用中&#xff0c;不同…

【量化交易实战记】小明的破晓时刻——2023下半年新能源汽车板块的成功掘金之旅

在2023年的炎炎夏日&#xff0c;小明在不断的观察分析市场的过程中&#xff0c;突然敏锐地察觉到了新能源汽车市场的风云变幻。他日复一日地研读行业报告、追踪政策动向、分析公司财报&#xff0c;以及密切关注全球市场动态。那段时间里&#xff0c;新能源汽车行业仿佛迎来了一…

Vue中父子组件通信

聚沙成塔每天进步一点点 本文内容 ⭐ 专栏简介Vue中父子组件通信1. Props父组件&#xff1a;子组件&#xff1a; 2. 自定义事件子组件&#xff1a;父组件&#xff1a; 3. 使用 v-model子组件&#xff1a;父组件&#xff1a; 4. 使用$refs子组件&#xff1a;父组件&#xff1a; …

必示科技助力中国联通智网创新中心通过智能化运维(AIOps)通用能力成熟度3级评估

2023年12月15日&#xff0c;中国信息通信研究院隆重公布了智能化运维AIOps系列标准最新批次评估结果。 必示科技与中国联通智网创新中心合作的“智能IT故障监控定位分析能力建设项目”通过了中国信息通信研究院开展的《智能化运维能力成熟度系列标准 第1部分&#xff1a;通用能…

通用外设-2.8‘TFT屏的使用

前言 一、验证连接是否正确 二、更改自己想用的图像 1.取模软件 Image2Lcd 2.9 的使用 2.使用 总结 前言 本文在中景园的代码上改写而来&#xff0c;主要记录下使用记录 一、验证连接是否正确 1.按内容说明进行线路连接 2.运行程序&#xff0c;因为内部有图片样本&…

这可能是最全面的Java并发编程八股文了

内容摘自我的学习网站&#xff1a;topjavaer.cn 分享50道Java并发高频面试题。 线程池 线程池&#xff1a;一个管理线程的池子。 为什么平时都是使用线程池创建线程&#xff0c;直接new一个线程不好吗&#xff1f; 嗯&#xff0c;手动创建线程有两个缺点 不受控风险频繁创…

23年全球数字经济发展如何?这本《白皮书》告诉你答案丨附下载

这一年&#xff0c;全球主要国家优化数字经济政策布局&#xff0c; 促进数字产业化创新升级、发展数字基础设施&#xff1b; 这一年&#xff0c;全域国际合作让“命运共同体” 构建见成效&#xff0c; 全球经济多极化趋势加强&#xff0c;中国坐拥Top1数字市场&#xff1b; …

第二证券:抢占技术前沿 中国光伏企业结伴“走出去”

2024年新年前后&#xff0c;光伏职业分外忙碌。据证券时报记者不完全统计&#xff0c;晶澳科技、华晟新动力、高测股份、华民股份等多家企业宣告新建项目投产&#xff0c;安徽皇氏绿能等企业的项目也迎来设备安装的重要节点。 证券时报记者采访多家企业的负责人后了解到&#…

js日期排序(使用sort)

根据日期进行排序&#xff0c;也可以根据number类型的大小来进行排序 按日期排序的函数 let data [{id: 2,time: 2019-04-26 10:53:19},{id: 4,time: 2019-04-26 10:51:19}, {id: 1,time: 2019-04-26 11:04:32}, {id: 3,time: 2019-04-26 11:05:32} ] //property是你需要排序…

一款好用的开源思维导图软件 docker部署教程

目录 Simple mind map简介 Simple mind map特点 1.拉取镜像 2.创建并启动容器 方式1&#xff1a;docker启动 方式2&#xff1a;docker compose启动 3.使用 4.源码地址 Simple mind map简介 .一个 Web 思维导图&#xff0c;基于思维导图库、Vue2.x、ElementUI 开发&#…

前端框架前置课Node.js学习(1) fs,path,模块化,CommonJS标准,ECMAScript标准,包

目录 什么是Node.js 定义 作用: 什么是前端工程化 Node.js为何能执行Js fs模块-读写文件 模块 语法: 1.加载fs模块对象 2.写入文件内容 3.读取文件内容 Path模块-路径处理 为什么要使用path模块 语法 URL中的端口号 http模块-创建Web服务 需求 步骤: 案例:浏…