MIT 6.S081 Lab Four

news2024/7/31 0:24:16

MIT 6.S081 Lab Four

  • 引言
  • traps
    • RISC-V assembly (easy)
      • 代码解析
    • Backtrace(moderate)
      • 代码解析
    • Alarm(Hard)
      • test0: invoke handler(调用处理程序)
      • test1/test2(): resume interrupted code(恢复被中断的代码)
      • 代码解析
        • issue解答
    • 可选的挑战练习


引言

本文为 MIT 6.S081 2020 操作系统 实验四解析。

MIT 6.S081课程前置基础参考: 基于RISC-V搭建操作系统系列


traps

本实验探索如何使用陷阱实现系统调用。您将首先使用栈做一个热身练习,然后实现一个用户级陷阱处理的示例。

Attention

  • 开始编码之前,请阅读xv6手册的第4章和相关源文件:
    • *kernel/trampoline.S*:涉及从用户空间到内核空间再到内核空间的转换的程序集
    • *kernel/trap.c*:处理所有中断的代码

要启动实验,请切换到traps分支:

$ git fetch
$ git checkout traps
$ make clean

RISC-V assembly (easy)

理解一点RISC-V汇编是很重要的,你应该在6.004中接触过。xv6仓库中有一个文件user/call.c。执行make fs.img编译它,并在user/call.asm中生成可读的汇编版本。

阅读call.asm中函数gfmain的代码。RISC-V的使用手册在参考页上。以下是您应该回答的一些问题(将答案存储在answers-traps.txt文件中):

  1. 哪些寄存器保存函数的参数?例如,在mainprintf的调用中,哪个寄存器保存13?
  2. main的汇编代码中对函数f的调用在哪里?对g的调用在哪里(提示:编译器可能会将函数内联)
  3. printf函数位于哪个地址?
  4. mainprintfjalr之后的寄存器ra中有什么值?
  5. 运行以下代码。
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);

程序的输出是什么?这是将字节映射到字符的ASCII码表。

输出取决于RISC-V小端存储的事实。如果RISC-V是大端存储,为了得到相同的输出,你会把i设置成什么?是否需要将57616更改为其他值?

这里有一个小端和大端存储的描述和一个更异想天开的描述。

  1. 在下面的代码中,“y=”之后将打印什么(注:答案不是一个特定的值)?为什么会发生这种情况?
printf("x=%d y=%d", 3);

代码解析

call.c代码如下所示:

#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int g(int x) {
  return x+3;
}

int f(int x) {
  return g(x);
}

void main(void) {
  printf("%d %d\n", f(8)+1, 13);
  exit(0);
}
  1. 哪些寄存器保存函数的参数?例如,在mainprintf的调用中,哪个寄存器保存13?
    在这里插入图片描述
  • 在a0-a7中存放参数,13存放在a2中

  1. main的汇编代码中对函数f的调用在哪里?对g的调用在哪里(提示:编译器可能会将函数内联)
    • 在C代码中,main调用f,f调用g。而在生成的汇编中,main函数进行了内联优化处理。
    • 从代码li a1,12可以看出,main直接计算出了结果并储存

  1. printf函数位于哪个地址?
    • 在0x630

  1. mainprintfjalr之后的寄存器ra中有什么值?
  • auipc(Add Upper Immediate to PC):auipc rd imm,将高位立即数加到PC上,从下面的指令格式可以看出,该指令将20位的立即数左移12位之后(右侧补0)加上PC的值,将结果保存到dest位置,图中为rd寄存器

在这里插入图片描述

  • jalr (jump and link register):jalr rd, offset(rs1)跳转并链接寄存器。jalr指令会将当前PC+4保存在rd中,然后跳转到指定的偏移地址offset(rs1)。

在这里插入图片描述

来看XV6的代码:

  30: 00000097       auipc ra,0x0
  34: 600080e7       jalr  1536(ra) # 630 <printf>

第一行代码:

  • 00000097H=00...0 0000 1001 0111B,对比指令格式,可见imm=0,dest=00001,opcode=0010111,对比汇编指令可知,auipc的操作码是0010111,ra寄存器代码是00001。
  • 这行代码将0x0左移12位(还是0x0)加到PC(当前为0x30)上并存入ra中,即ra中保存的是0x30

第2行代码:

  • 600080e7H=0110 0...0 1000 0000 1110 0111B,可见imm=0110 0000 0000,rs1=00001,funct3=000,rd=00001,opcode=1100111,rs1和rd的操作码都是00001,即都为寄存器ra。
  • 这对比jalr的标准格式有所不同,可能是此两处使用寄存器相同时,汇编中可以省略rd部分。
  • ra中保存的是0x30,加上0x600后为0x630,即printf的地址,执行此行代码后,将跳转到printf函数执行,并将PC+4=0X34+0X4=0X38保存到ra中,供之后返回使用。

  1. 程序的输出是什么?

    • 57616=0xE110,0x00646c72小端存储为72-6c-64-00,对照ASCII码表
    • 72:r 6c:l 64:d 00:充当字符串结尾标识
    • 因此输出为:HE110 World
    • 若为大端存储,i应改为0x726c6400,不需改变57616

  1. 在下面的代码中,“y=”之后将打印什么(注:答案不是一个特定的值)?为什么会发生这种情况?
    • 原本需要两个参数,却只传入了一个,因此y=后面打印的结果取决于之前a2中保存的数据

Backtrace(moderate)

回溯(Backtrace)通常对于调试很有用:它是一个存放于栈上用于指示错误发生位置的函数调用列表。

kernel/printf.c中实现名为backtrace()的函数。在sys_sleep中插入一个对此函数的调用,然后运行bttest,它将会调用sys_sleep。你的输出应该如下所示:

backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898

bttest退出qemu后。在你的终端:地址或许会稍有不同,但如果你运行addr2line -e kernel/kernel(或riscv64-unknown-elf-addr2line -e kernel/kernel),并将上面的地址剪切粘贴如下:

$ addr2line -e kernel/kernel
0x0000000080002de2
0x0000000080002f4a
0x0000000080002bfc
Ctrl-D

你应该看到类似下面的输出:

kernel/sysproc.c:74
kernel/syscall.c:224
kernel/trap.c:85

​ 编译器向每一个栈帧中放置一个帧指针(frame pointer)保存调用者帧指针的地址。你的backtrace应当使用这些帧指针来遍历栈,并在每个栈帧中打印保存的返回地址。

提示:

  • kernel/defs.h中添加backtrace的原型,那样你就能在sys_sleep中引用backtrace
  • GCC编译器将当前正在执行的函数的帧指针保存在s0寄存器,将下面的函数添加到kernel/riscv.h
static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x) );
  return x;
}

并在backtrace中调用此函数来读取当前的帧指针。这个函数使用内联汇编来读取s0

  • 这个课堂笔记中有张栈帧布局图。注意返回地址位于栈帧帧指针的固定偏移(-8)位置,并且保存的帧指针位于帧指针的固定偏移(-16)位置

在这里插入图片描述
XV6在内核中以页面对齐的地址为每个栈分配一个页面。你可以通过PGROUNDDOWN(fp)PGROUNDUP(fp)(参见kernel/riscv.h)来计算栈页面的顶部和底部地址。这些数字对于backtrace终止循环是有帮助的。

一旦你的backtrace能够运行,就在kernel/printf.cpanic中调用它,那样你就可以在panic发生时看到内核的backtrace


代码解析

在这里插入图片描述

这个函数就是实现曾经调用函数地址的回溯,这个功能在日常的编程中也经常见到,编译器报错时就是类似的逻辑,只不过题目的要求较为简单,只用打印程序地址,而实际的报错中往往打印程序文件名,函数名以及行号等信息(最后的可选练习就是实现这样的功能):

/**
 * @brief backtrace 回溯函数调用的返回地址
 */
void
backtrace(void) {
  printf("backtrace:\n");
  // 读取当前帧指针
  uint64 fp = r_fp();
  while (PGROUNDUP(fp) - PGROUNDDOWN(fp) == PGSIZE) {
    // 返回地址保存在-8偏移的位置
    uint64 ret_addr = *(uint64*)(fp - 8);
    printf("%p\n", ret_addr);
    // 前一个帧指针保存在-16偏移的位置
    fp = *(uint64*)(fp - 16);
  }
}

根据提示:返回地址位于栈帧帧指针的固定偏移(-8)位置,并且保存的帧指针位于帧指针的固定偏移(-16)位置。先使用r_fp()读取当前的帧指针,然后读出返回地址并打印,再将fp定位到前一个帧指针的位置继续读取即可。

根据提示:XV6在内核中以页面对齐的地址为每个栈分配一个页面。使用PGROUNDUP(fp) - PGROUNDDOWN(fp) == PGSIZE判断当前
fp是否被分配了一个页面来终止循环。

对于入口函数,因为它没有上一级函数调用,所以没有需要回溯的上一级栈帧。然而,根据RISC-V函数调用约定的一致性,入口函数的栈帧中仍然会保留一个上一级栈帧的值。

在RISC-V中,栈的增长方向是向下的,即栈指针(SP)递减。因此,在入口函数的栈帧中,上一级栈帧的值通常被设置为入口函数自身的栈指针(SP)的初始值。

因为入口函数地址是栈的起始地址,是对齐的地址,所以在经过向上和向下取整后,得到的结果是一样的,因此相减为0,这时候我们就知道整个函数调用链结束了。


Alarm(Hard)

YOUR JOB

  • 在这个练习中你将向XV6添加一个特性,在进程使用CPU的时间内,XV6定期向进程发出警报。这对于那些希望限制CPU时间消耗的受计算限制的进程,或者对于那些计算的同时执行某些周期性操作的进程可能很有用。更普遍的来说,你将实现用户级中断/故障处理程序的一种初级形式。
  • 例如,你可以在应用程序中使用类似的一些东西处理页面故障。如果你的解决方案通过了alarmtest和usertests就是正确的。

你应当添加一个新的sigalarm(interval, handler)系统调用,如果一个程序调用了sigalarm(n, fn),那么每当程序消耗了CPU时间达到n个“滴答”,内核应当使应用程序函数fn被调用。当fn返回时,应用应当在它离开的地方恢复执行。在XV6中,一个滴答是一段相当任意的时间单元,取决于硬件计时器生成中断的频率。如果一个程序调用了sigalarm(0, 0),系统应当停止生成周期性的报警调用。

你将在XV6的存储库中找到名为user/alarmtest.c的文件。将其添加到Makefile

  • 注意:你必须添加了sigalarmsigreturn系统调用后才能正确编译(往下看)。

alarmtesttest0中调用了sigalarm(2, periodic)来要求内核每隔两个滴答强制调用periodic(),然后旋转一段时间。你可以在user/alarmtest.asm中看到alarmtest的汇编代码,这或许会便于调试。当alarmtest产生如下输出并且usertests也能正常运行时,你的方案就是正确的:

$ alarmtest
test0 start
........alarm!
test0 passed
test1 start
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
test1 passed
test2 start
................alarm!
test2 passed
$ usertests
...
ALL TESTS PASSED
$

当你完成后,你的方案也许仅有几行代码,但如何正确运行是一个棘手的问题。我们将使用原始存储库中的alarmtest.c版本测试您的代码。你可以修改alarmtest.c来帮助调试,但是要确保原来的alarmtest显示所有的测试都通过了。


test0: invoke handler(调用处理程序)

首先修改内核以跳转到用户空间中的报警处理程序,这将导致test0打印“alarm!”。不用担心输出“alarm!”之后会发生什么;如果您的程序在打印“alarm!”后崩溃,对于目前来说也是正常的。以下是一些提示

  • 您需要修改Makefile以使alarmtest.c被编译为xv6用户程序。
  • 放入user/user.h的正确声明是:
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
  • 更新user/usys.pl(此文件生成user/usys.S)、kernel/syscall.hkernel/syscall.c以允许alarmtest调用sigalarmsigreturn系统调用。
  • 目前来说,你的sys_sigreturn系统调用返回应该是零。
  • 你的sys_sigalarm()应该将报警间隔和指向处理程序函数的指针存储在struct proc的新字段中(位于kernel/proc.h)。
  • 你也需要在struct proc新增一个新字段。用于跟踪自上一次调用(或直到下一次调用)到进程的报警处理程序间经历了多少滴答;您可以在proc.callocproc()中初始化proc字段。
  • 每一个滴答声,硬件时钟就会强制一个中断,这个中断在kernel/trap.c中的usertrap()中处理。
  • 如果产生了计时器中断,您只想操纵进程的报警滴答;你需要写类似下面的代码
if(which_dev == 2) ...
  • 仅当进程有未完成的计时器时才调用报警函数。请注意,用户报警函数的地址可能是0(例如,在***user/alarmtest.asm***中,periodic位于地址0)。
  • 您需要修改usertrap(),以便当进程的报警间隔期满时,用户进程执行处理程序函数。当RISC-V上的陷阱返回到用户空间时,什么决定了用户空间代码恢复执行的指令地址?
  • 如果您告诉qemu只使用一个CPU,那么使用gdb查看陷阱会更容易,这可以通过运行
make CPUS=1 qemu-gdb
  • 如果alarmtest打印“alarm!”,则您已成功。

test1/test2(): resume interrupted code(恢复被中断的代码)

alarmtest打印“alarm!”后,很可能会在test0test1中崩溃,或者alarmtest(最后)打印“test1 failed”,或者alarmtest未打印“test1 passed”就退出。要解决此问题,必须确保完成报警处理程序后返回到用户程序最初被计时器中断的指令执行。必须确保寄存器内容恢复到中断时的值,以便用户程序在报警后可以不受干扰地继续运行。最后,您应该在每次报警计数器关闭后“重新配置”它,以便周期性地调用处理程序。

作为一个起始点,我们为您做了一个设计决策:用户报警处理程序需要在完成后调用sigreturn系统调用。请查看alarmtest.c中的periodic作为示例。这意味着您可以将代码添加到usertrapsys_sigreturn中,这两个代码协同工作,以使用户进程在处理完警报后正确恢复。

提示:

  • 您的解决方案将要求您保存和恢复寄存器——您需要保存和恢复哪些寄存器才能正确恢复中断的代码?(提示:会有很多)
  • 当计时器关闭时,让usertrapstruct proc中保存足够的状态,以使sigreturn可以正确返回中断的用户代码。
  • 防止对处理程序的重复调用——如果处理程序还没有返回,内核就不应该再次调用它。test2测试这个。
  • 一旦通过test0test1test2,就运行usertests以确保没有破坏内核的任何其他部分。

代码解析

只给出核心代码,相关头文件声明和makefile声明不再详述。

这项练习要实现定期的警报。首先是要通过test0,如何调用处理程序是主要的问题。程序计数器的过程是这样的:

  1. ecall指令中将PC保存到SEPC
  2. usertrap中将SEPC保存到p->trapframe->epc
  3. p->trapframe->epc加4指向下一条指令
  4. 执行系统调用
  5. usertrapret中将SEPC改写为p->trapframe->epc中的值
  6. sret中将PC设置为SEPC的值

可见执行系统调用后返回到用户空间继续执行的指令地址是由p->trapframe->epc决定的,因此在usertrap中主要就是完成它的设置工作。

(1). 在struct proc中增加字段,同时记得在allocproc中将它们初始化为0,并在freeproc中也设为0

//kernel/proc.h
int alarm_interval;          // 报警间隔
void (*alarm_handler)();     // 报警处理函数
int ticks_count;             // 两次报警间的滴答计数

(2). 在sys_sigalarm中读取参数

//kernel/sysproc.c
uint64
sys_sigalarm(void) {
  if(argint(0, &myproc()->alarm_interval) < 0 ||
    argaddr(1, (uint64*)&myproc()->alarm_handler) < 0)
    return -1;

  return 0;
}

(3). 修改usertrap()

// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
    if(++p->ticks_count == p->alarm_interval) {
        // 更改陷阱帧中保留的程序计数器
        p->trapframe->epc = (uint64)p->alarm_handler;
        p->ticks_count = 0;
    }
    yield();
}

接下来要通过test1test2,要解决的主要问题是寄存器保存恢复和防止重复执行的问题。考虑一下没有alarm时运行的大致过程

  1. 进入内核空间,保存用户寄存器到进程陷阱帧
  2. 陷阱处理过程
  3. 恢复用户寄存器,返回用户空间

而当添加了alarm后,变成了以下过程

  1. 进入内核空间,保存用户寄存器到进程陷阱帧
  2. 陷阱处理过程
  3. 恢复用户寄存器,返回用户空间,但此时返回的并不是进入陷阱时的程序地址,而是处理函数handler的地址,而handler可能会改变用户寄存器

因此我们要在usertrap中再次保存用户寄存器,当handler调用sigreturn时将其恢复,并且要防止在handler执行过程中重复调用,过程如下

(1). 再在struct proc中新增两个字段

int is_alarming;                    // 是否正在执行告警处理函数
struct trapframe* alarm_trapframe;  // 告警陷阱帧

(2). 在allocproc和freeproc中设定好相关分配,回收内存的代码

/**
 * allocproc.c
 */
// 初始化告警字段
if((p->alarm_trapframe = (struct trapframe*)kalloc()) == 0) {
    freeproc(p);
    release(&p->lock);
    return 0;
}
p->is_alarming = 0;
p->alarm_interval = 0;
p->alarm_handler = 0;
p->ticks_count = 0;

/**
 * freeproc.c
 */
if(p->alarm_trapframe)
    kfree((void*)p->alarm_trapframe);
p->alarm_trapframe = 0;
p->is_alarming = 0;
p->alarm_interval = 0;
p->alarm_handler = 0;
p->ticks_count = 0;

(3). 更改usertrap函数,保存进程陷阱帧p->trapframep->alarm_trapframe

// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
  if(p->alarm_interval != 0 && ++p->ticks_count == p->alarm_interval && p->is_alarming == 0) {
    // 保存寄存器内容
    memmove(p->alarm_trapframe, p->trapframe, sizeof(struct trapframe));
    // 更改陷阱帧中保留的程序计数器,注意一定要在保存寄存器内容后再设置epc
    p->trapframe->epc = (uint64)p->alarm_handler;
    p->ticks_count = 0;
    p->is_alarming = 1;
  }
  yield();
}

(4). 更改sys_sigreturn,恢复陷阱帧

uint64
sys_sigreturn(void) {
  memmove(myproc()->trapframe, myproc()->alarm_trapframe, sizeof(struct trapframe));
  myproc()->is_alarming = 0;
  return 0;
}

issue解答

1.alarm_trapframe分配完物理地址空间后,不需要在内核页表建立映射关系吗?

  • kvminit函数中,内核已经将etext到PHYSTOP区域全部建立好了映射关系,确保内核能够访问到所有物理内存:
    在这里插入图片描述
    在这里插入图片描述
  • kalloc能够分配的物理内存区域在kernel data结束位置开始到PHYSTOP位置:

在这里插入图片描述

  • 当我们通过kalloc分配物理页存放alarm_trapframe时,我们需要再次建立映射关系,因为内核已经建立好了,并且由于alarm_trapframe不会再用户态下被访问,所以无需将alarm_trapframe映射到用户态页表中去。
  • 由于trapframe会作为从用户态切换到内核态的跳板,所以需要在用户态下访问,因此我们才需要将trapframe映射到用户态页表中去
    在这里插入图片描述
  • 因为alarm_trapframe的映射关系已经在内核页表中建立好了,所以后面的memmove函数才可以正常工作,在alarm_trapframe和trapframe之间进行数据的拷贝。

2.整个alarm的流程是什么?

  • 首先是sigalarm系统调用执行流程:
    在这里插入图片描述
  • sigalarm处理流程:

在这里插入图片描述


可选的挑战练习

在backtrace()中打印函数的名称和行号,而不仅仅是数字化的地址。(hard)

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

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

相关文章

「端午记忆,AI绘梦」微信群AI绘图比赛

点击上方「蓝字」&#xff0c;关注我们 01 活动介绍 亲爱的朋友们&#xff0c; 端午节即将来临&#xff0c;让我们一起用AI唤醒记忆&#xff0c;回忆古老传统的魅力。 这次活动&#xff0c;我们邀请大家进入一个微信群&#xff0c;一起用AI画出你记忆中端午的样子。 无论你是画…

一文通关Spring MVC

目录 &#x1f433;今日良言&#xff1a;少年负壮气&#xff0c;奋烈自有时 &#x1f433;一、Spring MVC的相关介绍 &#x1f415;1.Spring MVC的定义 &#x1f415;2.MVC 和 Spring MVC的关系 &#x1f433;二、Spring MVC的创建及使用 &#x1f42f;1.Spring MVC项目创…

Spring Boot 如何使用 Log4j2 进行日志记录

Spring Boot 如何使用 Log4j2 进行日志记录 在开发 Java 应用程序时&#xff0c;日志记录是非常重要的一环。Spring Boot 提供了多种日志输出方式&#xff0c;其中 Log4j2 是一种比较常用的日志框架。本文将介绍如何在 Spring Boot 应用程序中使用 Log4j2 进行日志记录。 为什…

Verilog基础:标识符的向上向下层次名引用

相关文章 Verilog基础&#xff1a;表达式位宽的确定&#xff08;位宽拓展&#xff09; Verilog基础&#xff1a;表达式符号的确定 Verilog基础&#xff1a;数据类型 Verilog基础&#xff1a;位宽拓展和有符号数运算的联系 Verilog基础&#xff1a;case、casex、ca…

基于阿尔法均值滤波的FPGA图像系统(工程+原理图+PCB+仿真)

目录 前言一、研究背景及意义二、本文研究内容三、硬件系统框架设计1、总框架设计2、原理图&PCB设计3、实物设计4、电路介绍 三、中值滤波算法研究及改进1、图像噪声的产生及危害2、中值滤波算法3、高斯滤波算法4、改进的中值滤波算法&#xff08;α均值滤波算法&#xff0…

【跑实验05】利用CLIP中的图像编码器,如何遍历文件夹中的图像,将图像文件改为28*28的尺寸,然后输出到excel中的每一列,最后一列全都标记为0

文章目录 一、初步实现二、警告信息的解决 一、初步实现 要遍历文件夹中的图像并将其尺寸调整为28x28&#xff0c;并将结果输出到Excel中&#xff0c;可以按照以下步骤进行操作&#xff1a; 首先&#xff0c;确保您已经安装了Pandas库&#xff0c;用于处理Excel文件。可以使用…

简单认识Nginx主配置文件及实操模拟

文章目录 一、Nginx主配置文件1、全局配置2、添加 I/O事件配置4.HTTP配置 实操模拟部分一、Nginx虚拟主机配置1.1基于域名1.2.基于IP1.3.基于端口 二、Nginx访问状态统计三、Nginx配置访问控制1.基于授权的访问控制2.基于客户端的访问控制 一、Nginx主配置文件 位置&#xff1…

【机器学习】sklearn数据集的使用,数据集的获取和划分

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 sklearn数据集 二、安装sklearn二、获取数据集三、…

python第三方库概览

目录 第三方库的获取和安装 脚本程序转变为可执行程序的第三方库PyInstaller jieba库(必选)、wordcloud库&#xff08;可选&#xff09; 知识导图&#xff1a; 1.Python第三方库的获取和安装 Python第三方库依照安装方式灵活性和难易程度有三个方法&#xff1a;pip工具安装…

树莓派使用VNC、SSH、Xrdp等方式进行远程控制的方法和注意事项

下面来总结一下远程操控树莓派用到的三种方式及其注意事项&#xff0c;其实这三种方式对于所有的Linux系统来说都是适用的。 目录 一、ssh控制树莓派 1.开启 ssh服务方法一 2.开启 ssh服务方法二 二、VNC远程连接 三、xrdp远程连接 四、其他注意事项 一、ssh控制树莓派 S…

石油化工领域生产作业流程合规检测 yolov8

石油化工领域生产作业流程合规检测通过引入yolov8视觉数据智能分析技术&#xff0c;石油化工领域生产作业流程合规检测对生产作业流程进行实时监测和合规性检测&#xff0c;通过与预设标准进行比对&#xff0c;系统能够检测出不合规的操作或异常情况&#xff0c;并及时发出警报…

【Python】实现一个鼠标连击器,每秒点击1000次

前言 鼠标连击是指在很短的时间内多次点击鼠标按钮&#xff0c;通常是鼠标左键。当触发鼠标连击时&#xff0c;鼠标按钮会迅速按下和释放多次&#xff0c;产生连续的点击效果。 在这里鼠标连击的主要用途是&#xff1a; 帮助我们进行鼠标点击&#xff0c;疯狂连击&#xff1…

NUCLEO-F411RE RT-Thread 体验 (6) - GCC环境 I2C驱动移植以及i2c-tool的编写

NUCLEO-F411RE RT-Thread 体验 (6) - GCC环境 I2C驱动移植以及i2c-tool的编写 1、I2C驱动移植 RT-Rhread这里用的是软件模拟i2c&#xff0c;stm32的驱动里并没有找到硬件i2c的驱动&#xff0c;但是在GD32里面却有硬件i2c的驱动&#xff0c;有兴趣的小伙伴可以根据gd32的代码写…

Ubutun开机黑屏解决方法

开机黑屏解决方法 临时性解决方法永久性解决方法补充说明 在项目支持过程中发现Ubuntu 16 在新终端上开机黑屏&#xff0c;没有显示图形界面&#xff0c;这个可能是因为系统版本太低&#xff0c;对新显卡不兼容导致的&#xff0c;后通过查资料有如下解决方法。 临时性解决方法 …

腾讯云+PicGo+Typora图床,生成专属图片链接

腾讯云PicGoTypora搭建自己的图床 原创声明&#xff0c;转载请注明文章链接来源、作者信息 TyporaPicGogitHub搭建自己的图床&#xff0c;写作效率大大提升 索奇问答 问&#xff1a;图床是什么&#xff1f; 答&#xff1a;用户可以将图片上传到图床&#xff0c;然后将生成的…

代码随想录算法训练营第四十一天 |

01背包&#xff1a;n种物品&#xff0c;每种物品只有1个&#xff0c;有相应的重量和价值 最多只能装m的重量&#xff0c;最多价值为多少&#xff1f; dp[i][j] : [0, i]物品任取放进容量为j的背包里 不放物品i&#xff1a;dp[i-1][j] 放物品i&#xff1a;dp[i-1][j-weight[…

递归的浅浅应用

首先&#xff0c;这里是一道简单题目&#xff0c;浅浅地验证了我之前发过的这篇文章 写递归题目的思路 &#xff0c;我结合它来讲解一下这道题的思路&#xff1a; 剑指 Offer 27 和 method 226.翻转二叉树 给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;…

python数据清洗 —— re.split()划分字符串

需求 对于一行字符串&#xff1a; route-views6.routeviews.org 141694 2a0c:b641:24f:fffe::7 184891 | CN | apnic | OTAKUJAPAN-AS Otaku Limited, CN要将其划分成如下7个部分&#xff0c; [route-views6.routeviews.org, 141694…

【C++篇】OOP下部分:友元、运算符重载与多态

友情链接&#xff1a;C/C系列系统学习目录 知识总结顺序参考C Primer Plus&#xff08;第六版&#xff09;和谭浩强老师的C程序设计&#xff08;第五版&#xff09;等&#xff0c;内容以书中为标准&#xff0c;同时参考其它各类书籍以及优质文章&#xff0c;以至减少知识点上的…

精选MyBatis面试题

什么是MyBatis&#xff1f; MyBatis是一个半ORM&#xff08;对象关系映射&#xff09;框架&#xff0c;它内部封装了JDBC&#xff0c;加载驱动、创建连接、创建statement等繁杂的过程&#xff0c;开发者开发时只需要关注如何编写SQL语句&#xff0c;可以严格控制sql执行性能&a…