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

news2024/11/27 0:27:11

系列文章目录

操作系统入门系列-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系统的实际启动流程,但是对学习现实中操作系统的实际启动流程有帮助

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

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

相关文章

数据库系统概论(个人笔记)(第四部分)

数据库系统概论&#xff08;个人笔记&#xff09; 文章目录 数据库系统概论&#xff08;个人笔记&#xff09;4、中间的SQL4.1 连接表达式4.2 视图4.3 事务4.4 完整性约束4.5 SQL数据类型和模式4.6 SQL中的索引定义4.7 授权 4、中间的SQL 4.1 连接表达式 Join Expressions Join…

10.无代码爬虫软件做网页数据抓取流程——工作流程设置与数据预览

首先&#xff0c;多数情况下免费版本的功能&#xff0c;已经可以满足绝大多数采集需求&#xff0c;想了解八爪鱼采集器版本区别的详情&#xff0c;请访问这篇帖子&#xff1a;https://blog.csdn.net/cctv1123/article/details/139581468 八爪鱼采集器免费版和个人版、团队版下…

视频监控平台:通过网络SDK对TCL网络摄像机进行PTZ控制 的源代码介绍及分享

目录 一、视频监控平台介绍 &#xff08;一&#xff09;概述 &#xff08;二&#xff09;视频接入能力介绍 &#xff08;三&#xff09;功能介绍 二、TCL网络摄像机 &#xff08;一&#xff09;360度全景自动旋转&#xff1a; &#xff08;二&#xff09;高清夜视和全彩…

五、路由协议

目录 一、为何划分子网&#xff1f; 二、静态路由 三、OSPF 3.1、OSPF原理 3.1.1、ospf四步过程 3.2、OSPF区域划分 3.2.1、为什么划区域&#xff1f; 3.2.2、划分规则 3.3、OSPF参数 3.4、五种报文 3.4.1、hello报文 3.4.2、DD报文 3.4.2、LSR、LSU、LSAck报文 3…

黄仁勋:下一波AI的浪潮是物理AI

B站&#xff1a;啥都会一点的研究生公众号&#xff1a;啥都会一点的研究生 最近AI圈又发生了啥&#xff1f; 快手视频生成大模型“可灵”开放邀测&#xff0c;效果对标 Sora 在OpenAl文生视频大模型Sora发布后&#xff0c;国内企业争相入局&#xff0c;快手视频生成大模型可…

MySQL之优化服务器设置和复制(一)

优化服务器设置 操作系统状态 CPU密集型的机器 CPU密集型服务器的vmstat输出通常在us列会有一个很高的值&#xff0c;报告了花费在非内核代码上的CPU时钟;也可能在sy列有很高的值&#xff0c;表示系统CPU利用率&#xff0c;超过20%就足以令人不安了。在大部分情况下&#xff…

pyqt 鼠绘椭圆 椭圆标注

目录 pyqt 椭圆标注 四个方向可以调整,调整时,另一端固定,只调整当前端,椭圆参数保存加载json pyqt 画椭圆中心点固定,调整是,两端一起调整。 pyqt 椭圆标注 四个方向可以调整,调整时,另一端固定,只调整当前端,椭圆参数保存加载json import sys import json from …

React@16.x(32)useDebugValue

目录 1&#xff0c;介绍2&#xff0c;作用 1&#xff0c;介绍 从一个例子开始&#xff1a; export default function App() {const [n, setN] useState(0);const refH1 useRef();useEffect(() > {console.log("父组件");});return <h1 ref{refH1}>{n}&l…

C++语法02 输出语句与分隔

目录 标准输出语句 题目&#xff1a;小知的位置 小知的位置参考代码 如何输出算式的结果&#xff1f; 题目&#xff1a;难题 难题参考代码 如何分隔多个算式输出的内容&#xff1f; 题目&#xff1a;两道题目 两道题目参考代码 标准输出语句 标准输出指令&#xff1a…

一小时搞定JavaScript(1)——JS基础

前言,本篇文章是依据bilibili博主(波波酱老师)的学习笔记,波波酱老师讲的很好,很适合速成!!! 文章目录 1.基本语法1.1 JS语言的特点1.2 创建一个JS文件1.3 输入与输出1.4 变量1.4.1 命名规则1.4.2 变量声明数字类型字符串类型布尔类型未定义类型 1.4.3 类型转换隐式转换显示转…

【html】如何利用id选择器实现主题切换

今天给大家介绍一种方法来实现主题切换的效果 效果图&#xff1a; 源码&#xff1a; <!DOCTYPE html> <html lang"zh"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initia…

【Linux 12】进程控制

文章目录 &#x1f308; Ⅰ 进程创建01. fork 函数介绍02. 写时拷贝03. fork 常规用法04. fork 调用失败的原因 &#x1f308; Ⅱ 进程终止01. 进程退出场景02. 常见退出方法 &#x1f308; Ⅲ 进程等待01. 进程等待必要性02. 进程等待的方法2.1 wait 方法2.2 waitpid 方法 03.…

AI大模型浪潮席卷而来,你准备好乘风破浪成为行业翘楚了吗?

揭秘AI大模型浪潮&#xff1a;你准备好乘风破浪了吗&#xff1f; 在繁华的都市中&#xff0c;程序员小李一直默默耕耘在代码的海洋中。然而&#xff0c;随着AI大模型技术的迅猛发展&#xff0c;他发现自己仿佛置身于一场没有硝烟的战争中。身边的同事纷纷掌握了新技术&#xf…

AI绘图StableDiffusion最强大模型盘点 - 诸神乱战

玩了这么久的StableDiffusion&#xff0c;Civitai和HF上的各种大模型和LORA也都基本玩了个遍。 自己也一直想做一期盘点&#xff0c;选出我自己心中最好或者最有意思的那几个大模型。 毕竟每次看着模型库里几十个大模型&#xff0c;是个人都遭不住。 我在这篇文章中&#xf…

C++之模板(一)

1、为什么需要模板 将具有相同逻辑的一段代码提供一份模板&#xff0c;当我们需要处理不同类型的时候&#xff0c;可以通过数据类型当作参数来传递&#xff0c;从而实例化出对应类型的处理版本。 2、模板的定义 也是一种静态多态。 3、模板的分类 4、函数模板 5、函数模板的使…

大模型企业落地:汽车行业知识大模型应用

前言 在当今这个信息爆炸的时代&#xff0c;知识管理成为了企业提升核心竞争力的关键。特别是在汽车行业这样一个技术密集、信息量庞大的领域&#xff0c;如何高效管理和利用知识资源&#xff0c;成为了每个企业必须面对的挑战。 汽车行业的知识管理痛点 汽车行业作为现代工…

百货商场:打造品质生活

走进我们的百货商场&#xff0c;仿佛置身于一个五彩斑斓的梦幻世界。百货&#xff0c;不仅仅是购物的场所&#xff0c;更是一种品质生活的体验。 在这里&#xff0c;您可以找到最适合自己的商品选择。从家居用品到时尚服饰&#xff0c;从美食佳肴到美妆护肤&#xff0c;每一样商…

多态深度剖析

前言 继承是多态的基础&#xff0c; 如果对于继承的知识还不够了解&#xff0c; 可以去阅读上一篇文章 继承深度剖析 基本概念与定义 概念&#xff1a; 通俗来说&#xff0c;就是多种形态。具体点就是去完成某个行为&#xff0c; 当不同的对象去完成时会产生出不同的状…

数据资产入表-数据分级分类标准-数据分类

2021年9月1日&#xff0c;《中华人民共和国数据安全法》正式施行&#xff0c;明确规定“国家建立数据分类分级保护制度”&#xff0c;数据分级分类是数据安全管理的重要措施&#xff0c;它涉及到对数据资产的识别、分类和定级&#xff0c;是保障数据合规的前提。 数据分类&…

物联网主机 E6000 在智慧工地上的应用

随着科技的不断发展&#xff0c;智慧工地的概念逐渐普及。物联网技术的应用为工地管理带来了革命性的变化&#xff0c;物联网主机E6000作为一款领先的物联网主机设备&#xff0c;在智慧工地上发挥着重要作用。 物联网主机 E6000 是一种集成了多种传感器和通信技术的设备。支持融…