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 statevoid *chan;                  // If non-zero, sleeping on chanint killed;                  // If non-zero, have been killedint xstate;                  // Exit status to be returned to parent's waitint 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 stackuint64 sz;                   // Size of process memory (bytes)pagetable_t pagetable;       // User page tablestruct trapframe *trapframe; // data page for trampoline.Sstruct usyscall *usyscall_page;struct context context;      // swtch() here to run processstruct file *ofile[NOFILE];  // Open filesstruct inode *cwd;           // Current directorychar 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 pageif((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 onlyif(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 pagefor(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 可以看到测试用例全部通过: 

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

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

相关文章

DDSP-SVC-3.0完全指南:一步步教你用AI声音开启音乐之旅

本教程教你怎么使用工具训练数据集推理出你想要转换的声音音频&#xff0c;并且教你处理剪辑伴奏和训练后的音频合并一起&#xff0c;快来试试看把&#xff01; 1.使用的工具 要想训练ai声音&#xff0c;首先需要有各种工具&#xff0c;还需要我们提供你需要训练的声音&#…

Avalonia中如何将View事件映射到ViewModel层

前言 前面的文章里面我们有介绍在Wpf中如何在View层将事件映射到ViewModel层的文章&#xff0c;传送门&#xff0c;既然WPF和Avalonia是两套不同的前端框架&#xff0c;那么WPF里面实现模式肯定在这边就用不了&#xff0c;本篇我们将分享一下如何在Avalonia前端框架下面将事件…

陀螺仪LSM6DSV16X与AI集成(2)----姿态解算

陀螺仪LSM6DSV16X与AI集成.2--姿态解算 概述视频教学样品申请完整代码下载欧拉角万向节死锁四元数法姿态解算双环PI控制器偏航角陀螺仪解析代码上位机通讯加速度演示陀螺仪工作方式主程序演示 概述 LSM6DSV16X包含三轴陀螺仪与三轴加速度计。 姿态有多种数学表示方式&#xff…

多人聊天室

多人聊天包 由于要先创建服务面板&#xff0c;接收客户端连接的信息&#xff0c;此代码使用顺序为先启动服务端&#xff0c;在启动客户端&#xff0c;服务端不用关&#xff0c;不然会报错。多运行几次客户端&#xff0c;实现单人聊天 1.创建服务面板 package yiduiy;import j…

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

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

【头歌系统数据库实验】实验6 SQL的多表查询-2

目录 第1关&#xff1a;查询每个选手的信息及其提交的解答信息&#xff0c;没做题的选手不显示 第2关&#xff1a;查询做了1001题且耗时大于500&#xff08;time&#xff09;的选手信息 第3关&#xff1a;查询所有选手信息及其提交的解答信息&#xff0c;没做题的选手也要显…

力扣每日一题:2646. 最小化旅行的价格总和(2023-12-06)

力扣每日一题 题目&#xff1a;2646. 最小化旅行的价格总和 日期&#xff1a;2023-12-06 用时&#xff1a;30 m 14 s 时间&#xff1a;8ms 内存&#xff1a;42.98MB 思路&#xff1a;先统计旅行中每个节点路过的次数&#xff08;dfs方法&#xff09;&#xff0c;再计算减半后的…

项目中使用之Maven BOM

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; 工具教程 ✨特色专栏&#xff1a; MyS…

Linux文件部分知识

目录 认识inode 如何理解创建一个空文件&#xff1f; 如何理解对文件写入信息&#xff1f; 如何理解删除一个文件&#xff1f; 为什么拷贝文件的时候很慢&#xff0c;而删除文件的时候很快&#xff1f; 如何理解目录 ​编辑 文件的三个时间 ​编辑 Access&#xff1a; …

Linux系统调试课:网络性能工具总结

文章目录 一、网络性能指标二、netstat三、route四、iptables沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇章一起了解下网络性能工具。 一、网络性能指标 从网络性能指标出发,你更容易把性能工具同系统工作原理关联起来,对性能问题有宏观的认识和把握。这样,…

【LeetCode刷题-链表】--92.反转链表II

92.反转链表II /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}* ListNode(int val) { this.val val; }* ListNode(int val, ListNode next) { this.val val; this.next next; }* }*/ cla…

前端JavaScript入门-day08-正则表达式

目录 介绍 语法 元字符 边界符 量词 字符类&#xff1a; 修饰符 介绍 正则表达式&#xff08;Regular Expression&#xff09;是用于匹配字符串中字符组合的模式。在 JavaScript中&#xff0c;正则表达式也是对象&#xff0c;通常用来查找、替换那些符合正则表达式的…

书-用数组存储高于60低于70的人单独存起来

#include<stdio.h> # define N 10 //书-用数组存储高于60低于70的人单独存起来 int main(){float s[N]{68.2,62.3,63.4,34.5,45.6,56.7,67.8,78.9,89.0,100};int i;float diyu[100];int j0;for(i0;i<N;i){if(s[i]>60 && s[i]<70)diyu[j]s[i];//这里的范…

【数据结构】——二叉树特点

前言&#xff1a;我们前面已经了解了二叉树的一些概念&#xff0c;那么我们今天就来了解下二叉树的遍历实现和一些性质。 二叉树的遍历方式有三种&#xff1a;前序&#xff0c;中序&#xff0c;后序。 前序&#xff1a;先根节点&#xff0c;再左子树&#xff0c;最后右子树。 中…

绘制6层及以上PCB板,需要明白PCB板的结构和叠层

PCB主要由PP半固化片和core芯板压合而成&#xff0c;其中core芯板两面都有铜箔&#xff0c;是PCB板的导电介质&#xff1b;PP半固化片是绝缘材料&#xff0c;用于芯板的粘合。 在PP半固化片被层压后&#xff0c;其环氧树脂被挤压开来&#xff0c;将core芯板粘合在一起。 PCB的叠…

Python代码将txt里面多行json字符串转成excel文件

python 代码 将txt里面的多行json字符串转成excel history.txt文件json代码样例 Json转换Excel代码 import json import pandas as pddef json_out(file_path,excel_path):all_list[]with open(file_path, "r", encodingutf-8) as f:for line in f:all_list.append…

Tuxera NTFS2024安装包下载教程

在听到小凡的电话说“Tuxera NTFS for Mac软件安装失败&#xff0c;怎么办”的时候&#xff0c;小编心里真像有一万头草泥马在奔腾——苹果软件还能安装失败&#xff01;&#xff1f; 挥手把一万头草泥马赶走&#xff0c;脑补着苹果软件的安装&#xff0c;小编对着电话吼道&am…

配置主机与外网时间服务器同步时间

目录 NTP服务简介 时间管理命令 第一步&#xff1a;更改当前主机所在地的时间 方法一&#xff1a;使用tzselect命令查询需要的时区 1、使用tzselect命令查询需要的时区 2、添加变量到 ~/.bash_profile 文件中&#xff0c;即追加类似的内容&#xff1a; 3、重新连接一个…

LLM大语言模型(一):ChatGLM3-6B本地部署

目录 前言 本机环境 ChatGLM3代码库下载 模型文件下载 修改为从本地模型文件启动 启动模型网页版对话demo 超参数设置 GPU资源使用情况 &#xff08;网页对话非常流畅&#xff09; 前言 LLM大语言模型工程化&#xff0c;在本地搭建一套开源的LLM&#xff0c;方便后续的…

深入了解UUID:生成、应用与优势

一、引言 在当今数字化时代&#xff0c;唯一标识一个对象的能力变得越来越重要。UUID&#xff08;Universally Unique Identifier&#xff0c;通用唯一标识符&#xff09;应运而生&#xff0c;作为一种保证全球唯一性的标识方法&#xff0c;广泛应用于各种场景。本文将详细介绍…