操作系统入门系列-MIT6.828(操作系统工程)学习笔记(六)---- 初窥操作系统启动流程(xv6启动)

系列文章目录

操作系统入门系列-MIT6.S081(操作系统)学习笔记(一)---- 操作系统介绍与接口示例
操作系统入门系列-MIT6.828(操作系统工程)学习笔记(二)----课程实验环境搭建(wsl2+ubuntu+quem+xv6)
操作系统入门系列-MIT6.828(操作系统工程)学习笔记(三)---- xv6初探与实验一(Lab: Xv6 and Unix utilities)
操作系统入门系列-MIT6.828(操作系统工程)学习笔记(四)---- C语言与计算机架构(Programming xv6 in C)
操作系统入门系列-MIT6.828(操作系统工程)学习笔记(五)---- 操作系统的组织结构(OS design)
操作系统入门系列-MIT6.828(操作系统工程)学习笔记(六)---- 初窥操作系统启动流程(xv6启动)


文章目录

  • 系列文章目录
  • 前言
  • 一、xv6操作系统的启动
    • 1.使用GDB跟踪启动流程
    • 2.程序入口——支持C语言(为执行C语言代码作准备)
    • 2.start.c——模式切换与赋权(为启动作准备)
    • 3.main——初始化系统功能
    • 4.userinit——准备第一个用户进程启动
    • 5.initcode.S——第一个用户进程
    • 6.init.c——初始化用户空间
    • 7.shell——各种应用程序运行
  • 总结


前言

本节对应的是MIT 6.828课程第三节:OS design
有大佬讲视频课程的内容进行了中文记录,链接如下:MIT6.828 简介
按照课程官方的进度安排:课程进度计划表
一、课前预习:
1.读xv6实验指导手册第二章
2.精读xv6源码: kernel/proc.h, kernel/defs.h, kernel/entry.S, kernel/main.c, user/initcode.S, user/init.c
3.略读xv6源码: kernel/proc.c and kernel/exec.c
二、课后任务:
1.完成Lab: system calls

本文主要探究xv6操作系统的启动流程,进而初步窥视操作系统的启动。因为xv6是一个以教育为目的的精简操作系统,所以xv6的启动并不能完全与实际操作系统的启动一致,本文作为“抛砖引玉”之用。


一、xv6操作系统的启动

启动的大致流程图如下:
在这里插入图片描述
系统调用的前置只是查看:
操作系统入门系列-MIT6.S081(操作系统)学习笔记(一)---- 操作系统介绍与接口示例

1.使用GDB跟踪启动流程

gdb可以帮助我们跟踪代码的执行,老师就是使用gdb进行跟踪。但是本文以“上帝视角”直接进行代码讲解,后面的实验会展示gdb的使用过程。

(1)首先在实验目录下使用命令:

make qemu-gdb

在这里插入图片描述
(2)打开另一个窗口,在实验目录下输入:

gdb-multiarch

gdb就启动了:
在这里插入图片描述

2.程序入口——支持C语言(为执行C语言代码作准备)

在实验目录下,查看文件kernel/kernel.asm,这个文件中存储着被编译后链接起来的kernel的汇编代码。
可以看到,kernel程序的起始地址是0x8000_0000
地址0x80000000是一个被QEMU认可的地址,也就是说如果你想使用QEMU,那么第一个指令地址必须是它。所以,我们会让内核加载器从那个位置开始加载内核,这一规定写入kernel.ld文件中,指导链接器工作。

在这里插入图片描述
初始代码是来自汇编文件kernel/entry.S,如下:该程序的功能就是先为每一个CPU分配一个4096byte的栈空间,之后转到start.c文件执行。

        # qemu -kernel loads the kernel at 0x80000000. qemu -kernel 命令在0x80000000处加载内核代码到# and causes each hart (i.e. CPU) to jump there. 并且使每个核心(即CPU)跳转到那里# kernel.ld causes the following code to. # be placed at 0x80000000.# 链接脚本 kernel.ld 使得下面的代码被放置在地址 0x80000000。
.section .text
.global _entry
_entry:# set up a stack for C. 为C语言建立一个栈空间# stack0 is declared in start.c, stack0在start.c文件中被申明# with a 4096-byte stack per CPU. 每个CPU都分配有4096(4K)的栈空间# sp = stack0 + (hartid * 4096)la sp, stack0li a0, 1024*4csrr a1, mhartidaddi a1, a1, 1mul a0, a0, a1add sp, sp, a0# jump to start() in start.c 转到statr.c执行call start
spin:j spin

2.start.c——模式切换与赋权(为启动作准备)

从kernel.asm文件可以看到,调用start函数的指令为:

jal	ra,8000589e <start>

即跳转到0x800589e处执行代码,我们再顺着查看kernel.asm文件0x800589e处的代码,以及kernel/start.c文件:

在这里插入图片描述
下面是start.c的代码,具体的代码讲解在注释中。大致上来讲,strat.c的作用是:当前内核代码还运行在机器权限下,仅仅只是初始化了栈空间来支持C语言。start的目的是将芯片的权限由机器模式( machine mode)转变为监管模式(Supervisor mode),也就是kernel mode。并且在转变之前,将内存管理、中断与异常、时钟的管理权限给监管模式。最后在切换到监管模式后,跳转到main函数执行。

// entry.S jumps here in machine mode on stack0. entry.S
// 程序跳转至该函数,在机器模式下在stack0栈空间执行start函数
void start()
{// set M Previous Privilege mode to Supervisor, for mret./*设置 M Previous Privilege mode 为 Supervisor,以便于 mret 指令的执行。在RISC-V中,mret指令用于从机器模式(Machine Mode)返回到之前的特权模式。为了确保mret指令能够正确返回到Supervisor模式(S模式)需要在执行mret之前将M模式的Previous Privilege mode设置为Supervisor模式。这样,当mret指令执行时,处理器会从机器模式返回到Supervisor模式。*/unsigned long x = r_mstatus();x &= ~MSTATUS_MPP_MASK;x |= MSTATUS_MPP_S;w_mstatus(x);// set M Exception Program Counter to main, for mret.// requires gcc -mcmodel=medany// 将M异常程序计数器设置为main函数的地址,以便于mret指令的执行。// 需要使用gcc编译器,并指定-mcmodel=medany选项。/*在RISC-V架构中,为了使用mret指令从机器模式(M模式)返回到之前的特权模式(例如S模式或U模式)需要将M模式的异常程序计数器(Exception Program Counter, EPC)设置为要返回的程序的入口点。在这个例子中,EPC被设置为指向main函数的地址这样当执行mret指令时,处理器会跳转到main函数的起始位置继续执行。此外,需要使用gcc编译器,并且需要指定特定的编译选项-mcmodel=medany。这个选项告诉编译器使用中等大小的代码模型(medium code model)这允许代码和数据在更大的地址范围内进行访问。这对于某些嵌入式系统或需要较大地址空间的应用程序来说是必要的。*/w_mepc((uint64)main);// disable paging for now.// 当前暂时禁用分页机制w_satp(0);// delegate all interrupts and exceptions to supervisor mode.// 将所有中断和异常委托给管理模式。/*在RISC-V架构中,操作系统或固件设置的意图是将所有的中断和异常处理委托给管理模式(Supervisor Mode)在RISC-V中,管理模式是介于用户模式(User Mode)和机器模式(Machine Mode)之间的一种特权模式它允许操作系统执行一些受保护的操作,比如处理中断和异常。通过将中断和异常委托给管理模式操作系统可以更有效地管理这些事件,执行必要的处理比如调度任务、处理I/O请求等这种委托机制是通过设置特定的控制和状态寄存器来实现的确保当发生中断或异常时,处理器能够自动切换到管理模式,并执行相应的处理程序。在RISC-V中,这种机制是通过设置机器模式下的控制寄存器(如mideleg和medeleg)来实现的这些寄存器允许操作系统指定哪些中断和异常应该被委托给管理模式处理。*/w_medeleg(0xffff);w_mideleg(0xffff);w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);// configure Physical Memory Protection to give supervisor mode// access to all of physical memory.// 配置物理内存保护,使得管理模式能够访问所有物理内存。/*在RISC-V架构中,操作系统或固件正在配置物理内存保护(Physical Memory Protection, PMP)机制以便于管理模式(Supervisor Mode)能够访问整个物理内存空间PMP是RISC-V架构中用于控制对物理内存访问权限的一种机制它允许操作系统或固件定义一系列的内存区域并为每个区域指定访问权限,如读、写和执行。通过配置PMP,可以实现对物理内存的精细控制确保只有授权的代码和数据可以被访问,从而提高系统的安全性和稳定性在statr函数中,操作系统或固件正在设置PMP,以允许管理模式访问所有物理内存这通常是为了执行某些需要广泛内存访问权限的操作,比如初始化内存、管理内存映射等。配置PMP通常涉及设置特定的控制寄存器如PMP配置寄存器(PMPADDRn)和PMP配置控制寄存器(PMPCFGn)这些寄存器定义了内存区域的边界和访问权限。*/w_pmpaddr0(0x3fffffffffffffull);w_pmpcfg0(0xf);// ask for clock interrupts.// 请求时钟中断timerinit();// keep each CPU's hartid in its tp register, for cpuid().// 将每个CPU的hart ID存储在其tp(thread pointer)寄存器中,以便于cpuid()函数使用。/*在RISC-V架构中,hart ID是一个标识符,用于唯一标识系统中的每个CPU核心tp寄存器通常用于存储线程相关的数据,如线程局部存储(Thread-Local Storage, TLS)的基地址通过将hart ID存储在tp寄存器中,操作系统或应用程序可以方便地通过访问tp寄存器来获取当前执行线程的CPU核心信息。cpuid()被设计用来返回当前执行线程的CPU的hart ID通过这种方式,操作系统或应用程序可以实现对不同CPU核心的负载均衡、资源分配和性能监控等功能。*/int id = r_mhartid();w_tp(id);// switch to supervisor mode and jump to main().// 切换到管理模式并跳转到 main() 函数。asm volatile("mret");
}

3.main——初始化系统功能

根据kernel.asm代码:执行完start函数后,将会到main函数(0x80000362)执行

 asm volatile("csrw mepc, %0" : : "r" (x));800058be:	ffffb797          	auipc	a5,0xffffb800058c2:	aa478793          	addi	a5,a5,-1372 # 80000362 <main>800058c6:	34179073          	csrw	mepc,a5

下面是对汇编代码的解释,来自于海螺AI,是国产的免费的大模式,个人觉得很好用,安利一下:
1.asm 关键字表示这是一个内联汇编语句
2.volatile 关键字表示这个内联汇编语句不应该被编译器优化掉,即使它看起来没有改变任何变量的值。这通常用于执行一些有副作用的操作,比如修改硬件寄存器
3.“csrw mepc, %0” 是内联汇编指令本身。csrw 是一个RISC-V指令,用于将一个值写入到一个控制和状态寄存器(CSR)。mepc 是机器模式异常程序计数器(Machine Exception Program Counter),它保存了发生异常时应该返回的地址。%0 是一个占位符,表示内联汇编指令的第一个操作数。
4.: : “r” (x) 是内联汇编的输出操作数部分。它表示内联汇编指令的输出操作数,“r” 表示这个操作数应该被分配到一个通用寄存器中,(x) 是传递给内联汇编的变量。
5.auipc 是 “Add Upper Immediate to PC” 的缩写,它将一个立即数加到程序计数器(PC)上,并将结果存储在寄存器 a5 中。这里,0xffffb 是一个立即数,它被加到当前的PC上,以计算出一个绝对地址。
6.addi 是 “Add Immediate” 的缩写,它将一个立即数加到寄存器 a5 的值上。这里,-1372 是要加的立即数,它将 a5 寄存器的值调整为 main 函数的地址。
csrw mepc, a5 指令将 a5 寄存器的值(即 main 函数的地址)写入到 mepc 寄存器中。

总的来说,这段代码的目的是将 main 函数的地址写入到 mepc 寄存器中,这通常在异常处理或中断处理的上下文中发生,以便于在异常处理完成后能够返回到 main 函数继续执行。下面是kernel.asm中main函数的部分:
在这里插入图片描述
接着是kernel/main.c文件:细节的讲解写到了注释中,大致上就是在kernel模式下初始化一系列操作系统的功能,初始化完成后去执行userinit()函数

#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "defs.h"volatile static int started = 0;// start() jumps here in supervisor mode on all CPUs.
// 在所有CPU上,start() 函数在管理模式下跳转到这里
void
main()
{if(cpuid() == 0){//将读取和写入系统调用连接到控制台读取和控制台写入//初始化串口/*操作系统或系统软件中的一种配置,即把系统调用(system calls)中的读取(read)和写入(write)操作映射或关联到特定的函数或服务上。在操作系统中,系统调用是应用程序请求操作系统内核提供服务的一种机制例如,当应用程序需要从文件或设备读取数据时,它会调用系统提供的读取系统调用接口。在注释中提到的“consoleread”和“consolewrite”很是两个函数或服务它们分别用于处理控制台(即命令行界面)的读取和写入操作将系统调用“read”和“write”连接到这两个函数上意味着当应用程序执行读取或写入系统调用时实际上是在调用“consoleread”和“consolewrite”函数来处理与控制台的交互。这种配置通常在操作系统启动时进行,或者在系统初始化过程中设置它确保了应用程序可以通过标准的系统调用来与控制台进行交互,而无需直接操作硬件或底层细节这种抽象层使得应用程序的编写更加简单,同时允许操作系统在底层实现上进行优化或更改,而不会影响到应用程序的正常运行。*/consoleinit();//初始化printf函数printfinit();printf("\n");printf("xv6 kernel is booting\n");printf("\n");kinit();         // physical page allocator 设置好页表分配器(page allocator)kvminit();       // create kernel page table 设置好虚拟内存,这是下节课的内容kvminithart();   // turn on paging 打开页表,也是下节课的内容procinit();      // process table 设置好初始进程或者说设置好进程表单trapinit();      // trap vectors 初始化陷阱向量,在计算机系统中,//陷阱向量通常指的是操作系统或CPU架构中用于处理异常情况(如错误条件或中断)的机制//当发生异常或中断时,CPU会根据陷阱向量跳转到相应的处理程序,以处理这些事件。trapinithart();  // install kernel trap vector //加载内核的陷阱向量plicinit();      // set up interrupt controller //启动中断控制器plicinithart();  // ask PLIC for device interrupts 设置好中断控制器PLIC(Platform Level Interrupt Controller)//我们后面在介绍中断的时候会详细的介绍这部分,这是我们用来与磁盘和console交互方式binit();         // buffer cache 分配buffer cacheiinit();         // inode table 初始化inode缓存fileinit();      // file table 初始化文件系统virtio_disk_init(); // emulated hard disk 初始化磁盘userinit();      // first user process 最后当所有的设置都完成了,操作系统也运行起来了,会通过userinit运行第一个进程__sync_synchronize();started = 1;} else {while(started == 0);__sync_synchronize();printf("hart %d starting\n", cpuid());kvminithart();    // turn on pagingtrapinithart();   // install kernel trap vectorplicinithart();   // ask PLIC for device interrupts}scheduler();        
}

程序的最后会执行 scheduler()函数:它将执行一个进程,永不返回。这个进程就是一个用户进程,与userinit有关。

// Per-CPU process scheduler.
// Each CPU calls scheduler() after setting itself up.
// Scheduler never returns.  It loops, doing:
//  - choose a process to run.
//  - swtch to start running that process.
//  - eventually that process transfers control
//    via swtch back to the scheduler.
// 每个CPU的进程调度器。
// 每个CPU在完成自身设置后调用scheduler()函数。
// 调度器永不返回。它循环执行以下操作:
//   - 选择一个进程来运行。
//   - 通过swtch切换到该进程开始执行。
//   - 最终,该进程通过swtch将控制权交还给调度器。
void
scheduler(void)
{struct proc *p;struct cpu *c = mycpu();c->proc = 0;for(;;){// The most recent process to run may have had interrupts// turned off; enable them to avoid a deadlock if all// processes are waiting.// 最近运行的进程可能已经关闭了中断;// 为了防止所有进程都在等待时发生死锁,需要重新启用它们。intr_on();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;swtch(&c->context, &p->context);// Process is done running for now.// It should have changed its p->state before coming back.// 进程目前完成了运行。// 它应该在返回之前已经改变了其状态。c->proc = 0;}release(&p->lock);}}
}

4.userinit——准备第一个用户进程启动

在main()中,初始化完成后会进入到useinit函数,进行用户空间初始化,kenel.asm对应的部分代码如下:
在这里插入图片描述
userinit函数的C语言代码如下(proc.c文件中):

// Set up first user process.
// 启动第一个用户进程
void
userinit(void)
{struct proc *p;p = allocproc();//为第一个用户程序分配进程initproc = p;// allocate one user page and copy initcode's instructions// and data into it.// 分配一个用户页,并将初始化代码的指令和数据复制到该页(4K)中。uvmfirst(p->pagetable, initcode, sizeof(initcode));p->sz = PGSIZE;p->trace_mask = 0;//xxxxxxx// prepare for the very first "return" from kernel to user.// 准备进行从内核到用户空间的第一次“返回”p->trapframe->epc = 0;      // user program counter/*将进程的陷阱帧中的epc(Exception Program Counter)字段设置为0epc寄存器在RISC-V架构中用于存储发生异常或中断时的程序计数器(PC)值即下一条将要执行的指令的地址将epc设置为0意味着当进程从内核模式返回到用户模式时,它将从地址0开始执行这通常是一个异常情况,因为正常的程序不会从地址0开始执行。*/p->trapframe->sp = PGSIZE;  // user stack pointer/*将进程的陷阱帧中的 sp(Stack Pointer)字段设置为 PGSIZEsp 寄存器在RISC-V架构中用于存储当前栈顶的地址PGSIZE 通常定义为一个页面的大小,这表示进程的栈指针被设置为指向一个新页面的起始地址这为用户程序提供了一个初始的栈空间,以便于执行函数调用、局部变量存储等操作。*/safestrcpy(p->name, "initcode", sizeof(p->name));p->cwd = namei("/");p->state = RUNNABLE;release(&p->lock);
}

userinit有点像是胶水代码/Glue code(胶水代码不实现具体的功能,只是为了适配不同的部分而存在),它利用了XV6的特性,并启动了第一个进程。我们总是需要有一个用户进程在运行,这样才能实现与操作系统的交互,所以这里需要一个小程序来初始化第一个用户进程。这个小程序定义在initcode中:

5.initcode.S——第一个用户进程

uchar initcode[] = {0x17, 0x05, 0x00, 0x00, 0x13, 0x05, 0x45, 0x02,0x97, 0x05, 0x00, 0x00, 0x93, 0x85, 0x35, 0x02,0x93, 0x08, 0x70, 0x00, 0x73, 0x00, 0x00, 0x00,0x93, 0x08, 0x20, 0x00, 0x73, 0x00, 0x00, 0x00,0xef, 0xf0, 0x9f, 0xff, 0x2f, 0x69, 0x6e, 0x69,0x74, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00,0x00, 0x00, 0x00, 0x00
};

这里直接是程序的二进制形式,它会链接或者在内核中直接静态定义。实际上,这段代码对应了下面的汇编程序。


user/initcode.o:     file format elf64-littleriscvDisassembly of section .text:0000000000000000 <start>:
#include "syscall.h"# exec(init, argv)
.globl start
start:la a0, init0:	00000517          	auipc	a0,0x04:	00050513          	mv	a0,a0la a1, argv8:	00000597          	auipc	a1,0x0c:	00058593          	mv	a1,a1li a7, SYS_exec10:	00700893          	li	a7,7ecall14:	00000073          	ecall0000000000000018 <exit>:# for(;;) exit();
exit:li a7, SYS_exit18:	00200893          	li	a7,2ecall1c:	00000073          	ecalljal exit20:	ff9ff0ef          	jal	ra,18 <exit>0000000000000024 <init>:24:	696e692f          	0x696e692f28:	0074                	addi	a3,sp,12...000000000000002b <argv>:...

可以看到这个汇编程序的指令的初始地址就是0x00000000,结合userinit中的一个代码:

  p->trapframe->epc = 0;      // user program counter/*将进程的陷阱帧中的epc(Exception Program Counter)字段设置为0epc寄存器在RISC-V架构中用于存储发生异常或中断时的程序计数器(PC)值即下一条将要执行的指令的地址将epc设置为0意味着当进程从内核模式返回到用户模式时,它将从地址0开始执行这通常是一个异常情况,因为正常的程序不会从地址0开始执行。*/

从内核转到用户空间,将从地址0开始执行,也就是执行上述的汇编代码,汇编代码翻译为C语言如下:即使用exec系统调用执行init.c代码。

#include "syscall.h"
char init[]="/init\0";
char *argv[]={init, 0}
exec("init", argv);
for(;;) exit();

6.init.c——初始化用户空间

init.c已经是用户空间中的程序了,代码如下:该程序很简单,就是使用fork+exec的系统调用组合启动shell

// init: The initial user-level program#include "kernel/types.h"
#include "kernel/stat.h"
#include "kernel/spinlock.h"
#include "kernel/sleeplock.h"
#include "kernel/fs.h"
#include "kernel/file.h"
#include "user/user.h"
#include "kernel/fcntl.h"char *argv[] = { "sh", 0 };int
main(void)
{int pid, wpid;if(open("console", O_RDWR) < 0){mknod("console", CONSOLE, 0);open("console", O_RDWR);}dup(0);  // stdoutdup(0);  // stderrfor(;;){printf("init: starting sh\n");pid = fork();if(pid < 0){printf("init: fork failed\n");exit(1);}if(pid == 0){exec("sh", argv);printf("init: exec sh failed\n");exit(1);}for(;;){// this call to wait() returns if the shell exits,// or if a parentless process exits.wpid = wait((int *) 0);if(wpid == pid){// the shell exited; restart it.break;} else if(wpid < 0){printf("init: wait returned an error\n");exit(1);} else {// it was a parentless process; do nothing.}}}
}

7.shell——各种应用程序运行


总结

文章展示的是xv6启动的大致流程,有两个问题:(1)一些硬件和编译链接的细节没有体现(2)xv6是小型教学操作系统,只包含核心启动流程,并不是现实中诸如Linux系统的实际启动流程,但是对学习现实中操作系统的实际启动流程有帮助

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

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

相关文章

k8s离线部署Calico网络(2续)

下载离线镜像 百度网盘 链接&#xff1a;https://pan.baidu.com/s/14ReJW-ZyYZFLbwSEBZK6mA?pwdi6ct 提取码&#xff1a;i6ct 1.将离线镜像上传至所有服务器并解压&#xff1a; [rootmaster ~]# tar xf calico.tar.gz [rootmaster ~]# cd calico 2.所有服务器使用for循环导入…

ARM交叉编译

目录 一、介绍 1、本地编译 2、交叉编译 二、交叉工具链 1、概念 2、工具 3、获取方法 三、交叉编译运行程序 1、pc机操作&#xff08;x86_64&#xff09; ​2、开发板操作&#xff08;ARM&#xff09; 一、介绍 1、本地编译 本地编译是在与目标运行环境相同的机器上…

Vue3学习记录第三天

Vue3学习记录第三天 背景说明学习记录Vue3中shallowReactive()和shallowRef()Vue3中toRaw()和markRaw()前端...语法Vue3中readonly()和shallowReadonly()函数前端的防抖 背景 之前把Vue2的基础学了, 这个课程的后面有简单介绍Vue3的部分. 学习知识容易忘, 这里仅简答做一个记录…

【C++进阶】深入STL之 栈与队列:数据结构探索之旅

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ ⏩收录专栏⏪&#xff1a;C “ 登神长阶 ” &#x1f921;往期回顾&#x1f921;&#xff1a;模拟实现list与迭代器 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀stack和queue &#x1f4…

安利一款非常不错浏览器文本翻译插件(效果很不错,值得一试)

官网地址&#xff1a;https://immersivetranslate.com/ “沉浸式翻译”这个词&#xff0c;由我们发明创造。如今&#xff0c;它已然成为“双语对照翻译”的代名词。自2023年上线以来&#xff0c;这款备受赞誉的 AI 双语对照网页翻译扩展&#xff0c;已帮助超过 100 万用户跨越语…

uni-app uni-swipe-action 滑动操作状态恢复

按照uni-app官方文档的写法 当前同一条滑动确认之后 页面列表刷新 但是滑动的状态还在 入下图所示&#xff1a; 我们需要在滑动确认之后 页面刷新 滑动状态恢复 那么我们就来写一下这部分的逻辑&#xff1a; 首先&#xff0c;配置一下:show"isOpened[item.id]" chan…

探地雷达正演模拟,基于时域有限差分方法,二

回顾上一章的内容&#xff0c;首先是探地雷达的使用范围和其主要面向的地球物理勘探对象&#xff0c;其次是Maxwell方程及FDTD基础知识&#xff0c;本章的内容包括&#xff1a;1、基于C的TE波波动方程实现 2、边界问题的产生及处理。 一、基本波动方程实现&#xff1a; 使用C…

SpringBoot的Mybatis-plus实战之基础知识

文章目录 MybatisPlus 介绍一、MyBatisPlus 集成步骤第一步、引入依赖第二步、定义mapper 二、注解TableNameTableldTableField 三、配置文件四、加解密实现步骤 在SpringBoot项目中使用Mybatis-plus&#xff0c;记录下来&#xff0c;方便备查。 MybatisPlus 介绍 为简化开发而…

[数据集][目标检测]厨房积水检测数据集VOC+YOLO格式88张2类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;88 标注数量(xml文件个数)&#xff1a;88 标注数量(txt文件个数)&#xff1a;88 标注类别数…

【Activiti7系列】基于Spring Security的Activiti7工作流管理系统简介及实现(附源码)(下篇)

作者&#xff1a;后端小肥肠 上篇&#xff1a;【Activiti7系列】基于Spring Security的Activiti7工作流管理系统简介及实现&#xff08;上篇&#xff09;_spring security activiti7-CSDN博客 目录 1.前言 2. 核心代码 2.1. 流程定义模型管理 2.1.1. 新增流程定义模型数据 …

阻塞队列和线程池

一、什么是阻塞队列 1.1 什么是队列 队列是先进先出。 队列是一种特殊的线性表&#xff0c;特殊之处在于它只允许在表的前端&#xff08;front&#xff09;进行删除操作&#xff0c;而在表的后端&#xff08;rear&#xff09;进行插入操作&#xff0c;和栈一样&#xff0c;队…

Redis 双写一致原理篇

前言 我们都知道,redis一般的作用是顶在mysql前面做一个"带刀侍卫"的角色,可以缓解mysql的服务压力,但是我们如何保证数据库的数据和redis缓存中的数据的双写一致呢,我们这里先说一遍流程,然后以流程为切入点来谈谈redis和mysql的双写一致性是如何保证的吧 流程 首先…

10.1 Go Goroutine

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

transformer 位置编码源码解读

import torch import mathdef get_positional_encoding(max_len, d_model):"""计算位置编码参数&#xff1a;max_len -- 序列的最大长度d_model -- 位置编码的维度返回&#xff1a;一个形状为 (max_len, d_model) 的位置编码张量"""positional_e…

【机器学习】GPT-4中的机器学习如何塑造人类与AI的新对话

&#x1f680;时空传送门 &#x1f50d;引言&#x1f4d5;GPT-4概述&#x1f339;机器学习在GPT-4中的应用&#x1f686;文本生成与摘要&#x1f388;文献综述与知识图谱构建&#x1f6b2;情感分析与文本分类&#x1f680;搜索引擎优化&#x1f4b4;智能客服与虚拟助手&#x1…

27-LINUX--I/O复用-poll

一.poll概述 poll是一个多路复用的I/O模型&#xff0c;一个进程监视多个文件描述符&#xff0c;当文件描述符就绪时&#xff0c;poll返回可读并做相应处理。 1.poll的模型 #include <poll.h>struct pollfd {int fd; //文件描述符short events; //事件类型 s…

OpenAI新研究破解GPT-4大脑,分解1600万个特征打开“黑匣子”,Ilya 、Jan Leike也参与了!

6月7日凌晨&#xff0c;OpenAI在官网发布了一个新的研究成果&#xff0c;首次破解GPT-4的神经网络活动。通过改进大规模训练稀疏自动编码器将GPT-4的内部表示分解为 1600 万个特征。而且&#xff0c;前段时间离职的Ilya Sutskever、Jan Leike也是作者之一&#xff01; 这不是破…

将AIRNet集成到yolov8中,实现端到端训练与推理

AIRNet是一个图像修复网络,支持对图像进行去雾、去雨、去噪声的修复。其基于对比的退化编码器(CBDE),将各种退化类型统一到同一嵌入空间;然后,基于退化引导恢复网络(DGRN)将嵌入空间修复为目标图像。可以将AIRNet的输出与yolov8进行端到端集成,实现部署上的简化。 本博…

关于 Redis 中集群

哨兵机制中总结到&#xff0c;它并不能解决存储容量不够的问题&#xff0c;但是集群能。 广义的集群&#xff1a;只要有多个机器&#xff0c;构成了分布式系统&#xff0c;都可以称之为一个“集群”&#xff0c;例如主从结构中的哨兵模式。 狭义的集群&#xff1a;redis 提供的…

Django学习二:配置mysql,创建model实例,自动创建数据库表,对mysql数据库表已经创建好的进行直接操作和实验。

文章目录 前言一、项目初始化搭建1、创建项目&#xff1a;test_models_django2、创建应用app01 二、配置mysql三、创建model实例&#xff0c;自动创建数据库表1、创建对象User类2、执行命令 四、思考问题&#xff08;****&#xff09;1、是否会生成新表呢&#xff08;答案报错&…