操作系统入门系列-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, stack0
        li a0, 1024*4
        csrr a1, mhartid
        addi a1, a1, 1
        mul a0, a0, a1
        add 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,0xffffb
    800058c2:	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 cache
    iinit();         // 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 paging
    trapinithart();   // install kernel trap vector
    plicinithart();   // 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)字段设置为0
  epc寄存器在RISC-V架构中用于存储发生异常或中断时的程序计数器(PC)值
  即下一条将要执行的指令的地址
  将epc设置为0意味着当进程从内核模式返回到用户模式时,它将从地址0开始执行
  这通常是一个异常情况,因为正常的程序不会从地址0开始执行。
  */
  p->trapframe->sp = PGSIZE;  // user stack pointer
  /*
  将进程的陷阱帧中的 sp(Stack Pointer)字段设置为 PGSIZE
  sp 寄存器在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-littleriscv


Disassembly of section .text:

0000000000000000 <start>:
#include "syscall.h"

# exec(init, argv)
.globl start
start:
        la a0, init
   0:	00000517          	auipc	a0,0x0
   4:	00050513          	mv	a0,a0
        la a1, argv
   8:	00000597          	auipc	a1,0x0
   c:	00058593          	mv	a1,a1
        li a7, SYS_exec
  10:	00700893          	li	a7,7
        ecall
  14:	00000073          	ecall

0000000000000018 <exit>:

# for(;;) exit();
exit:
        li a7, SYS_exit
  18:	00200893          	li	a7,2
        ecall
  1c:	00000073          	ecall
        jal exit
  20:	ff9ff0ef          	jal	ra,18 <exit>

0000000000000024 <init>:
  24:	696e692f          	0x696e692f
  28:	0074                	addi	a3,sp,12
	...

000000000000002b <argv>:
	...

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

  p->trapframe->epc = 0;      // user program counter
  /*
  将进程的陷阱帧中的epc(Exception Program Counter)字段设置为0
  epc寄存器在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);  // stdout
  dup(0);  // stderr

  for(;;){
    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系统的实际启动流程,但是对学习现实中操作系统的实际启动流程有帮助

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

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

相关文章

SpringSecurity入门(三)

12、密码加密 12.1、不指定具体加密方式&#xff0c;通过DelegatingPasswordEncoder&#xff0c;根据前缀自动选择 PasswordEncoder passwordEncoder PasswordEncoderFactories.createDelegatingPasswordEncoder();12.2、指定具体加密方式 // Create an encoder with streng…

13. ESP32-HTTPClient(Arduino)

使用ESP32 Arduino框架的HTTPClient库进行HTTP请求 在ESP32开发里&#xff0c;网络通信是挺重要的一部分&#xff0c;你可能需要从服务器拿数据啊&#xff0c;或者把传感器数据发到云端什么的。不过别担心&#xff0c;ESP32 Arduino框架给我们提供了HTTPClient库&#xff0c;让…

阻塞队列和线程池

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

前端自测 - 那些经典的bug

前言 我一直坚持的一个观点&#xff0c;就是不以bug数论成败&#xff0c;但是这个需要加一个前提&#xff0c;就是不能出现那些低级的bug&#xff0c;更不能反复的出现。 由此整理了一系列我认为比较经典常见的前端bug&#xff0c;都是在项目中多次遇到过的&#xff0c;用于前…

Golang Context详解

文章目录 基本介绍context源码剖析Context接口emptyCtxcancelCtxtimerCtxvalueCtx context使用案例协程取消超时控制数据共享 基本介绍 基本介绍 在Go 1.7版本中引入了上下文&#xff08;context&#xff09;包&#xff0c;用于在并发编程中管理请求范围的数据、控制生命周期、…

vue-router全部搞定(附源码)

源码下载链接&#xff08;先转存&#xff0c;后下载&#xff09;&#xff1a;https://pan.quark.cn/s/b0c6edd68c21 怎么用vue-cli搭建项目 我们固然可以用传统htmljs的方式来搭建vue项目&#xff0c;但是如果组件很多&#xff0c;就需要通过Vue.component的方式一个个去引入…

Python的else子句7个妙用,原来还能这样用,整挺好!

## 1、条件语句else基础 &#x1f504; 1.1 简单else的常规操作 在Python中&#xff0c;else子句通常跟在if或一系列if-elif之后&#xff0c;提供一个“否则”的情况处理路径。如果前面的所有条件都不满足 &#xff0c;程序就会执行这里的代码块。例如 &#xff0c;检查一个数…

【STM32】GPIO输出(江科大)

一、GPIO简介 1.GPIO&#xff1a;通用输入输出口 2.可配置为8种输入输出模式 3.引脚电平&#xff1a;0-3.3V&#xff08;输出最大3.3V&#xff09;&#xff0c;部分引脚可容忍5V&#xff08;输入&#xff0c;有FT&#xff09; 4.输出模式下&#xff0c;可控制端口输出高低电平…

详解FedProx:FedAvg的改进版 Federated optimization in heterogeneous networks

FedProx&#xff1a;2020 FedAvg的改进 论文&#xff1a;《Federated Optimization in Heterogeneous Networks》 引用量&#xff1a;4445 源码地址&#xff1a; 官方实现&#xff08;tensorflow&#xff09;https://github.com/litian96/FedProx 几个pytorch实现&#xff1a;…

十二、【源码】配置注解执行SQL

源码地址&#xff1a;https://github.com/mybatis/mybatis-3/ 仓库地址&#xff1a;https://gitcode.net/qq_42665745/mybatis/-/tree/12-annotation 配置注解执行SQL 简化一下流程&#xff0c;主要可以分为下面几步&#xff1a; 1.解析配置&#xff0c;写入配置项 2.执行…

问题排查: Goalng Defer 带来的性能损耗

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 本作品 (李兆龙 博文, 由 李兆龙 创作)&#xff0c;由 李兆龙 确认&#xff0c;转载请注明版权。 文章目录 引言问题背景结论 引言 性能优化之路道阻且长&#xff0c;因为脱敏规定&#xff0c;…

Nginx 精解:正则表达式、location 匹配与 rewrite 重写

一、常见的 Nginx 正则表达式 在 Nginx 配置中&#xff0c;正则表达式用于匹配和重写 URL 请求。以下是一些常见的 Nginx 正则表达式示例&#xff1a; 当涉及正则表达式时&#xff0c;理解各个特殊字符的含义是非常重要的。以下是每个特殊字符的例子&#xff1a; ^&#xff1…

讯飞星火大模型个人API账号免费使用申请教程

文章目录 1.登录讯飞星火大模型官网 https://www.xfyun.cn/ 2.下滑找到Spark Lite&#xff0c;点击立即调用 3.星火大模型需要和具体的应用绑定&#xff0c;我们需要先创建一个新应用 https://console.xfyun.cn/app/myapp&#xff0c;应用名称可以按照自己的意愿起。 4.填写应用…

打造智慧工厂核心:ARMxy工业PC与Linux系统

智能制造正以前所未有的速度重塑全球工业格局&#xff0c;而位于这场革命核心的&#xff0c;正是那些能够精准响应复杂生产需求、高效驱动自动化流程的先进设备。钡铼技术ARMxy工业计算机&#xff0c;以其独特的设计哲学与卓越的技术性能&#xff0c;正成为众多现代化生产线背后…

ViT:2 理解CLIP

大模型技术论文不断&#xff0c;每个月总会新增上千篇。本专栏精选论文重点解读&#xff0c;主题还是围绕着行业实践和工程量产。若在某个环节出现卡点&#xff0c;可以回到大模型必备腔调或者LLM背后的基础模型新阅读。而最新科技&#xff08;Mamba,xLSTM,KAN&#xff09;则提…

vuInhub靶场实战系列--Kioptrix Level #3

免责声明 本文档仅供学习和研究使用,请勿使用文中的技术源码用于非法用途,任何人造成的任何负面影响,与本人无关。 目录 免责声明前言一、环境配置1.1 靶场信息1.2 靶场配置 二、信息收集2.1 主机发现2.1.1 netdiscover2.1.2 arp-scan主机扫描 2.2 端口扫描2.3 指纹识别2.4 目…

快速测试 Mybatis 复杂SQL,无需启动 Spring

快速测试mybatis的sql 当我们写完sql后&#xff0c;我们需要测试下sql是否符合预期&#xff0c;在填入各种参数后能否正常工作&#xff0c;尤其是对于复杂的sql。 一般我们测试可能是如下的代码: 由于需要启动spring&#xff0c;当项目较大的时候启动速度很慢&#xff0c;有些…

④-2单细胞学习-cellchat单数据代码补充版(通讯网络)

目录 通讯网络系统分析 ①社会网络分析 1&#xff0c;计算每个细胞群的网络中心性指标 2&#xff0c;识别细胞的信号流模式 ②非负矩阵分解&#xff08;NMF&#xff09;识别细胞的通讯模式 1&#xff0c;信号输出细胞的模式识别 2&#xff0c;信号输入细胞的模式识别 信…

RocketMq源码解析六:消息存储

一、消息存储核心类 rocketmq消息存储的功能主要在store这个模块下。 核心类就是DefaultMessageStore。我们看下其属性 // 配置文件 private final MessageStoreConfig messageStoreConfig; // CommitLog 文件存储实现类 private final CommitLog commitLog; …

【研发日记】Matlab/Simulink软件优化(三)——利用NaNFlag为数据处理算法降阶

文章目录 前言 背景介绍 初始算法 优化算法 分析和应用 总结 前言 见《【研发日记】Matlab/Simulink软件优化(一)——动态内存负荷压缩》 见《【研发日记】Matlab/Simulink软件优化(二)——通信负载柔性均衡算法》 背景介绍 在一个嵌入式软件开发项目中&#xff0c;需要开…