src/isa/riscv32/inst.c出发。

向上搜索,理解宏定义的含义。

R(i)

#define R(i) gpr(i)

  • R(i):访问第i号通用寄存器

会被替换为:

#define gpr(idx) (cpu.gpr[check_reg_idx(idx)])

分为两个部分:

  • cpu.gpr
  • check_reg_idx

cpu.gpr的每个含义,在预学习的时候已经接触过了。

对于check_reg_idx,可见参数为一个int,那么宏定义gprR的参数也是int

static inline int check_reg_idx(int idx) {IFDEF(CONFIG_RT_CHECK, assert(idx >= 0 && idx < MUXDEF(CONFIG_RVE, 16, 32)));return idx;
}

先看IFDEF

#define IFDEF(macro, ...) MUXDEF(macro, __KEEP, __IGNORE)(__VA_ARGS__)

又冒出来新的宏定义。

需要找MUXDEF

#define MUXDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)

又又冒出来新的宏定义。

如此递归,整理得到:

#define IFDEF(macro, ...) MUXDEF(macro, __KEEP, __IGNORE)(__VA_ARGS__)
#define MUXDEF(macro, X, Y)  MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)
#define MUX_MACRO_PROPERTY(p, macro, a, b) MUX_WITH_COMMA(concat(p, macro), a, b)
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
#define CHOOSE2nd(a, b, ...) b

CHOOSE2nd

递推的终点是:#define CHOOSE2nd(a, b, ...) b

从宏的名字和定义可以看出,这个宏的作用是:从可变参数中选择第二个参数。

测试一下,如果参数小于2怎么办。

error: macro "CHOOSE2nd" requires 3 arguments, but only 1 given6 |     cout << CHOOSE2nd(1) << endl;

报错信息虽然显示的是三个参数,但其实两个就够了。

MUX_WITH_COMMA

非常细节的逗号:

  • #define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)

无论如何都会选中b,意义何在?接着看看。

MUX_MACRO_PROPERTY

#define concat_temp(x, y) x ## y
#define concat(x, y) concat_temp(x, y)
#define CHOOSE2nd(a, b, ...) b
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
#define MUX_MACRO_PROPERTY(p, macro, a, b) MUX_WITH_COMMA(concat(p, macro), a, b)

经测试,无论前两个参数是啥,结果都是第四个参数。

  • 这个宏的作用是?接着看看

MUXDEF

#define MUXDEF(macro, X, Y) MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)

#include <iostream>
using namespace std;
#define concat_temp(x, y) x ## y
#define concat(x, y) concat_temp(x, y)
#define CHOOSE2nd(a, b, ...) b
#define MUX_WITH_COMMA(contain_comma, a, b) CHOOSE2nd(contain_comma a, b)
#define MUX_MACRO_PROPERTY(p, macro, a, b) MUX_WITH_COMMA(concat(p, macro), a, b)
#define __P_DEF_0  X,
#define __P_DEF_1  X,
#define __P_ONE_1  X,
#define __P_ZERO_0 X,
#define MUXDEF(macro, X, Y)  MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)
#define A
#define B 1
#define C 2int main() {cout << MUXDEF(A, 1, 2) << endl;cout << MUXDEF(B, 1, 2) << endl;cout << MUXDEF(C, 1, 2) << endl;cout << MUXDEF(1, 1, 2) << endl;cout << MUXDEF(0, 1, 2) << endl;
}

经测试,当拼接后的__P_DEF_macro有定义时,会返回X,否则返回Y

到这里,输出的结果就不再是固定的了。

回头看一下,依次展开:

MUXDEF(macro, X, Y)
MUX_MACRO_PROPERTY(__P_DEF_, macro, X, Y)
MUX_WITH_COMMA(concat(__P_DEF_, macro), X, Y)
CHOOSE2nd(__P_DEF_macro X, Y)
  • MUXDEF(macro, X, Y)会展开为:CHOOSE2nd(__P_DEF_macro X, Y)

但似乎还是只返回Y,为什么会返回X?看下面的函数:

#define concat_temp(x, y) x ## y
#define concat(x, y) concat_temp(x, y)

调用两个函数,结果是不一样的:

  • concat_temp(__P_DEF_, A)__P_DEF_A
  • concat(__P_DEF_, A)1,

哪来的逗号?

  • #define __P_DEF_1 X,
    • 非常细节的宏定义,X后有一个逗号。

concat(__P_DEF_, A)的展开结果为:

concat(__P_DEF_, A)
concat_temp(__P_DEF_, 1)
__P_DEF_1
X,

这个的X,MUX_WITH_COMMA省略的逗号结合。

如果A被定义为01,那么展开后,contain_comma a会变成X,a,使a成为第二个元素。

实际效果为宏定义下的?:三元运算符。

再回头看,那个流程图展开是有问题的。

宏定义不会递推到最后一层再展开,参考concat(__P_DEF_, A)的展开过程,A在第一步就展开了,它的展开结果会影响下一步展开。

对于整条链路的入口:MUXDEF(CONFIG_RVE, 16, 32))

  • 如果定义了CONFIG_RVE10,那么编译16,否则32

IFDEF

还有一个很费劲的宏定义,出现了三层括号。

#define __IGNORE(...)
#define __KEEP(...) __VA_ARGS__
#define IFDEF(macro, ...) MUXDEF(macro, __KEEP, __IGNORE)(__VA_ARGS__)

有一个非常关键的关键字:__VA_ARGS__

会取出可变参数的值,也就是...的部分。

比如IFDEF(A, cout<<1<<endl;),会先展开为:

  • MUXDEF(A, __KEEP, __IGNORE)(cout<<1<<endl;)

前文已经知道,MUXDEF在第一个参数定义为10时,会编译为第二个参数。

那么就变成了:

  • __KEEP(cout<<1<<endl;)

__KEEP会编译为参数列表,也就是:cout<<1<<endl;

第三个括号等第二个括号解析完成后作为参数传入。

总结

IFDEF(CONFIG_RT_CHECK, assert(idx >= 0 && idx < MUXDEF(CONFIG_RVE, 16, 32)));

  • 作用是判断,是否检查寄存器越界访问

R(i)

  • 作用是取出第i个寄存器的值。

一串宏定义的作用是判断取值的时候要不要检查。

Mr/Mw

#define Mr vaddr_read

这个函数在预学习的时候也用到过,现在顺着这个函数把宏定义捋一下。

首先是Mr后面没带括号,是给vaddr_read这个函数起了个别名。

vaddr_read是调用了paddr_read这个函数。

word_t paddr_read(paddr_t addr, int len) {if (likely(in_pmem(addr))) return pmem_read(addr, len);IFDEF(CONFIG_DEVICE, return mmio_read(addr, len));out_of_bound(addr);return 0;
}

现在又出现了多个宏定义。

likely

#define likely(cond) __builtin_expect(cond, 1)

告诉编译器,cond的值期望为1

__builtin_expect(expr, expected) 的返回值就是 expr 的值本身。

它的作用不是改变值,而是告诉编译器你“预期这个值通常为 expected(通常是 0 或 1)”,以便编译器做出更好的分支预测和优化。

static inline bool in_pmem(paddr_t addr) {return addr - CONFIG_MBASE < CONFIG_MSIZE;
}

in_pmem的作用是判断地址是否合法。通过与地址偏移量运算得到。

static word_t pmem_read(paddr_t addr, int len) {word_t ret = host_read(guest_to_host(addr), len);return ret;
}

pmem_read的作用是从客户机的物理内存地址addr开始,读取len字节的数据,并返回对应的值。

static inline word_t host_read(void *addr, int len) {switch (len) {case 1: return *(uint8_t  *)addr;case 2: return *(uint16_t *)addr;case 4: return *(uint32_t *)addr;IFDEF(CONFIG_ISA64, case 8: return *(uint64_t *)addr);default: MUXDEF(CONFIG_RT_CHECK, assert(0), return 0);}
}

len只有1,2,4,8四种取值。也就是取出addr开始的1,2,4,8个字节的数据。

default: MUXDEF(CONFIG_RT_CHECK, assert(0), return 0);

  • 如果定义了CONFIG_RT_CHECK,那么非法的len会触发断言
  • 如果未定义CONFIG_RT_CHECK,那么非法的len会被忽略,返回0

host_write的函数体与host_read逻辑类似。

static inline void host_write(void *addr, int len, word_t data) {switch (len) {case 1: *(uint8_t  *)addr = data; return;case 2: *(uint16_t *)addr = data; return;case 4: *(uint32_t *)addr = data; return;IFDEF(CONFIG_ISA64, case 8: *(uint64_t *)addr = data; return);IFDEF(CONFIG_RT_CHECK, default: assert(0));}
}

imm*

这段宏定义在下面的decode_operand()中使用。

BITS

#define BITS(x, hi, lo) (((x) >> (lo)) & BITMASK((hi) - (lo) + 1)) // similar to x[hi:lo] in verilog

  • 提取x的第hilo位(闭区间)

运算分为两个部分:((x) >> (lo))BITMASK((hi) - (lo) + 1)

先把低位干掉,然后取出新的地位。

BITMASK

#define BITMASK(bits) ((1ull << (bits)) - 1)

生成低bits位全是1的掩码。

ull避免溢出。

SEXT

#define SEXT(x, len) ({ struct { int64_t n : len; } __x = { .n = x }; (uint64_t)__x.n; })

写个程序测试一下功能。

({ ... })

  • 这是GCCClang支持的一种语法糖,用于将一个代码块作为一个表达式返回值。不能在标准C中使用。

在语法糖内部,有两条语句:

  1. struct { int64_t n : len; } __x = { .n = x };
    1. struct { int64_t n : len; }定义了一个匿名结构体,变量n只取第n位。
    2. __x = { .n = x }创建了一个结构体变量__x.n被赋值为x,高位会被截断。
  2. (uint64_t)__x.n;
    1. 把阶段的位域强转为uint64_t,并作为表达式结果。

那么SEXT的作用就是:

  • x看作一个len位的有符号整数,对其进行“符号扩展”为64位整数,并以uint64_t类型返回其值。

immI

#define immI() do { *imm = SEXT(BITS(i, 31, 20), 12); } while(0)

  1. 取出32位指令中的位段 [31:20]
  2. 将它作为12位 有符号立即数 符号扩展成64
  3. 然后赋值给*imm

immu

#define immU() do { *imm = SEXT(BITS(i, 31, 12), 20) << 12; } while(0)

  1. 取出32位指令中的位段 [31:12]
  2. 然后左移12位形成最终的32位立即数

imms

#define immS() do { *imm = (SEXT(BITS(i, 31, 25), 7) << 5) | BITS(i, 11, 7); } while(0)

  1. 取出:高7位:i[31:25]和低5位:i[11:7]
  2. 将高7位符号扩展,再左移5
  3. 与低5位做按位或,合并成完整的12位立即数

文献来源

  • https://drive.google.com/file/d/1uviu1nH-tScFfgrovvFCrj7Omv8tFtkp/view?usp=drive_link
  • Page26

decode_exec

函数里面嵌套宏定义的写法暂时看不懂。

向上搜索调用链:

int isa_exec_once(Decode *s) {s->isa.inst = inst_fetch(&s->snpc, 4);return decode_exec(s);
}

inst_fetch调用到vaddr_ifetch时,可以发现,与vaddr_read接下来的走向如出一辙。

int isa_exec_once(Decode *s) {s->isa.inst = inst_fetch(&s->snpc, 4);return decode_exec(s);
}
static inline uint32_t inst_fetch(vaddr_t *pc, int len) {uint32_t inst = vaddr_ifetch(*pc, len);(*pc) += len;return inst;
}

isa_exec_once的作用是,取出从&s->snpc处,长为4字节的指令。

  • 也就是32位指令。

并更新snpc为下一个位置。

snpcPA2手册中有提到:

snpc是下一条静态指令, 而dnpc是下一条动态指令. 对于顺序执行的指令, 它们的snpc和dnpc是一样的; 但对于跳转指令, snpc和dnpc就会有所不同, dnpc应该指向跳转目标的指令. 显然, 我们应该使用s->dnpc来更新PC, 并且在指令执行的过程中正确地维护s->dnpc

decode_exec

static int decode_exec(Decode *s) {s->dnpc = s->snpc;#define INSTPAT_INST(s) ((s)->isa.inst)
#define INSTPAT_MATCH(s, name, type, ... /* execute body */ ) { \int rd = 0; \word_t src1 = 0, src2 = 0, imm = 0; \decode_operand(s, &rd, &src1, &src2, &imm, concat(TYPE_, type)); \__VA_ARGS__ ; \
}INSTPAT_START();INSTPAT("??????? ????? ????? ??? ????? 00101 11", auipc  , U, R(rd) = s->pc + imm);INSTPAT("??????? ????? ????? 100 ????? 00000 11", lbu    , I, R(rd) = Mr(src1 + imm, 1));INSTPAT("??????? ????? ????? 000 ????? 01000 11", sb     , S, Mw(src1 + imm, 1, src2));INSTPAT("0000000 00001 00000 000 00000 11100 11", ebreak , N, NEMUTRAP(s->pc, R(10))); // R(10) is $a0INSTPAT("??????? ????? ????? ??? ????? ????? ??", inv    , N, INV(s->pc));INSTPAT_END();R(0) = 0; // reset $zero to 0return 0;
}

decode_exec的头部,把dnpc赋值为snpc。表示默认下一条指令就在下一个字节的位置。

中间两端宏定义暂时看不懂,但是好在暂时没有调用,只是定义:

#define INSTPAT_INST(s) ((s)->isa.inst)
#define INSTPAT_MATCH(s, name, type, ... /* execute body */ ) { \int rd = 0; \word_t src1 = 0, src2 = 0, imm = 0; \decode_operand(s, &rd, &src1, &src2, &imm, concat(TYPE_, type)); \__VA_ARGS__ ; \
}

可以接着往下看:

INSTPAT_START

第二条要执行的语句是:INSTPAT_START();

#define INSTPAT_START(name) { const void * __instpat_end = &&concat(__instpat_end_, name);

&&label是标签地址,官方文档链接:https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html

具体的功能可以写一个函数测试一下:

int main() {void *ptr = &&label1;goto *ptr;printf("hello world\n");
label1:printf("Jumped to label1!\n");return 0;
}

INSTPAT_START()传入的是空参数,展开的结果为:

{const void *__instpat_end = &&__instpat_end_;

非常细节的大括号,作用需要搭配INSTPAT_END()来理解:

__instpat_end_ :;
}

强制地提示,INSTPAT_START应与INSTPAT_END成对出现。

并且限制了作用域。

INSTPAT_END放置在函数体结尾。

INSTPAT

#define INSTPAT(pattern, ...) do { \uint64_t key, mask, shift; \pattern_decode(pattern, STRLEN(pattern), &key, &mask, &shift); \if ((((uint64_t)INSTPAT_INST(s) >> shift) & mask) == key) { \INSTPAT_MATCH(s, ##__VA_ARGS__); \goto *(__instpat_end); \} \
} while (0)

这段宏定义的内容是定义了一段代码,do...wihile保证按期望运行。

  • uint64_t key, mask, shift;声明了一些变量

pattern_decode(pattern, STRLEN(pattern), &key, &mask, &shift);

进到这个函数看下是如何运作的。

pattern_decode

定义了一堆宏定义,看起来比较复杂。

macro

#define macro(i) \if ((i) >= len) goto finish; \else { \char c = str[i]; \if (c != ' ') { \Assert(c == '0' || c == '1' || c == '?', \"invalid character '%c' in pattern string", c); \__key  = (__key  << 1) | (c == '1' ? 1 : 0); \__mask = (__mask << 1) | (c == '?' ? 0 : 1); \__shift = (c == '?' ? __shift + 1 : 0); \} \}

if ((i) >= len) goto finish;

len定义自:pattern_decode(pattern, STRLEN(pattern), &key, &mask, &shift);

也就是str的长度。

思考一下,macro64展开后能覆盖0-63,但字符串长度是64,支持的最大长度是63还是64

可以写个程序测试下。

字符串长度为64时输出了pattern too long

pattern_decode函数的作用是,从一个长度为len的字符串str中解析出三种信息:

  • key:把所有'0''1'字符组成一个位串,表示匹配值
  • mask:每一位如果是'0''1'则为1,如果是'?'则为0,表示哪些位需要匹配
  • shift:表示尾部连续'?'的数量,这些位会被右移舍弃掉

回到INSTPAT

if ((((uint64_t)INSTPAT_INST(s) >> shift) & mask) == key)

这里为什么key不用右移?

因为pattern_decode中已经右移过了:

finish:*key = __key >> __shift;*mask = __mask >> __shift;*shift = __shift;

指令匹配成功之后,会执行INSTPAT_MATCH,然后goto到结尾的位置。

类似一堆if-else

    INSTPAT_MATCH(s, ##__VA_ARGS__); \goto *(__instpat_end); \

INSTPAT_MATCH

INSTPAT_MATCH的入参为##__VA_ARGS__,在参数为空时,会自动去掉前面的逗号,避免编译报错。

#define INSTPAT_MATCH(s, name, type, ... /* execute body */ ) { \int rd = 0; \word_t src1 = 0, src2 = 0, imm = 0; \decode_operand(s, &rd, &src1, &src2, &imm, concat(TYPE_, type)); \__VA_ARGS__ ; \
}

发现nametype即使传入空置也不会影响目前的编译。

decode_operand

static void decode_operand(Decode *s, int *rd, word_t *src1, word_t *src2, word_t *imm, int type) {uint32_t i = s->isa.inst;int rs1 = BITS(i, 19, 15);int rs2 = BITS(i, 24, 20);*rd     = BITS(i, 11, 7);switch (type) {case TYPE_I: src1R();          immI(); break;case TYPE_U:                   immU(); break;case TYPE_S: src1R(); src2R(); immS(); break;case TYPE_N: break;default: panic("unsupported type = %d", type);}
}

第一个参数就是当前正在解码的指令上下文,封装了机器码值、指令地址等参数。

uint32_t i = s->isa.inst;
int rs1 = BITS(i, 19, 15);
int rs2 = BITS(i, 24, 20);
*rd     = BITS(i, 11, 7);

分别取出源寄存器1、源寄存器2和目的寄存器。

这三个寄存器的位置是固定的,在RSICV官方手册中的出处:

还是这张图。

每个寄存器不一定都能用到。但是每种类型的指令,只要用到了,位置就是固定的。

有个细节,上面的代码取寄存器的时候,只有rd是指针解引用赋值,其他参数是局部变量,对函数外暂时没有产生影响。

对于I型指令,需要immIrs1rd

对于U型指令,需要immUrd

对于S型指令,需要immSrs2rs1rd

  • rd对应手册中的imm[4:0],可以发现位置完全一样。

对于R型指令,看手册定义,格式与S型一致,猜测后续执行时会复用S型指令的逻辑。

到这里,decode_operand函数的意义已经非常明确了:

  • 根据不同的指令类型,取出操作数。

VA_ARGS

把可变参数展开。

结合已有代码:INSTPAT("??????? ????? ????? ??? ????? 00101 11", auipc , U, R(rd) = s->pc + imm);

首先会尝试与字符串模板匹配:"??????? ????? ????? ??? ????? 00101 11"

如果匹配成功,会展开INSTPAT_MATCH

  • s,在decode_exec函数入参中传入
  • nameINSTPAT的第二个参数auipc
  • typeINSTPAT的第三个参数U
  • ...INSTPAT的第四个参数R(rd) = s->pc + imm)

name目前来看无关紧要。

展开后会先根据type取出操作数。

然后展开...,操作取出的操作数。

总结

INSTPAT_STARTINSTPAT_END成对出现。

中间处理指令,某条规则匹配成功后,会立即执行并不再继续向下匹配。

INSTPAT的参数是:

  • 匹配规则
  • 指令名字
  • 指令类型
  • 执行语义,传入的应该是一系列函数。

参考

  • https://ysyx.oscc.cc/docs/ics-pa/2.2.html#rtfsc-2

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

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

相关文章

深度学习——手写数字识别

深度学习——手写数字识别 学习深度学习的朋友应该对MNIST数据集不陌生吧&#xff0c;相信很多人在刚开始学习深度学习的时候都会用到MNIST数据集进行书写数字识别。本篇文章参考鱼书创建一个深度网络来进行书写数字识别的任务。 如上图所示&#xff0c;这里使用的卷积层全都是…

HashMap算法高级应用实战:频率类子数组问题的5种破解模式

本文将深入剖析5种基于HashMap的高级模式&#xff0c;通过原理详解、多语言实现、性能对比和工业级应用&#xff0c;助您彻底掌握频率类子数组问题。 1. 深入解析&#xff1a;频率类子数组问题 1.1 问题定义与分类 频率类子数组问题是指需要统计或查找满足特定元素频率条件的…

【精选】计算机毕业设计HTML5智能宠物寻找与领养系统 跨平台宠物匹配 地图定位找宠 领养申请审核系统源码+论文+PPT+讲解

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

拼多多商家端 anti_content 补环境分析

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 部分python代码 import execjs impor…

电脑、手机长时间不关机可以吗

电脑和手机常年处于开机状态&#xff0c;只有在没电或者系统提示更新的时候才会关机。那问题来了&#xff0c;电脑、手机长时间不关机到底可不可以呢&#xff1f;今天咱们就来好好唠唠。 手机长时间不关机的情况 先来说说手机。现在的智能手机功能越来越强大&#xff0c;我们…

「AI大数据」| 《华为:面向智能制造的工业大模型标准化研究报告》

今天给大家介绍 《华为:面向智能制造的工业大模型标准化研究报告》&#xff0c;这是一份由中国电子技术标准化研究院联合华为等多家企业编写的权威报告&#xff0c;聚焦工业大模型在智能制造领域的标准化发展。报告详细分析了工业大模型的技术架构、应用场景、标准化现状与挑战…

Dinky1.2.3基于Kubernetes Application模式提交Flink作业

前言 Dinky 是一个开箱即用、易扩展&#xff0c;以 Apache Flink 为基础&#xff0c;连接 OLAP 和数据湖等众多框架的一站式实时计算平台&#xff0c;致力于流批一体和湖仓一体的探索与实践。 致力于简化Flink任务开发&#xff0c;提升Flink任务运维能力&#xff0c;降低Flink…

【软考高级架构设计师】——2025年上半年软考真题(回忆版)

目录 一、综合知识1.1、计算机基础与操作系统(15道单选)1.2、软件工程与架构(16道单选)1.3、数据与网络(8道单选)1.4、数学与逻辑(4道单选)1.5、其他(27道单选)1.6、英文题(质量属性)(5道单选)二、案例分析2.1、大模型训练系统(必选题)2.2、医院知识图谱(可选…

哈夫曼树Python实现

哈夫曼树构建原则&#xff1a; .统计频率&#xff1a;对待编码字符&#xff08;或数据块&#xff09;的频率进行统计。.初始化森林&#xff1a;将每个字符视为一棵只有根节点的二叉树&#xff0c;权值为频率。.合并树&#xff1a;重复以下操作&#xff0c;直到只剩一棵树&…

Dockerfile的学习与实践

Dockerfile通过一系列的命令和参数&#xff0c;构建自定义镜像。一般步骤如下&#xff1a; 一. 常用命令说明 基础命令具体命令描述例子FROMFROM[基础镜像:版本号]基于指定的基础镜像构建自定义镜像FROM eclipse-temurin:17-jdk-alpineRUNRUN构建容器需要运行的命令&#xff0…

【三大前端语言之一】静态网页语言:HTML详解

你知道你在浏览器中所看到的每一个按钮&#xff0c;每一个框&#xff0c;都是怎么创造出来的吗&#xff1f;它们并非魔法&#xff0c;而是由一种被称为HTML的语言精心构建的骨架。作为前端世界的三大基石之一&#xff08;HTML、CSS、JavaScript&#xff09;&#xff0c;HTML是万…

04、谁发明了深度学习的方法,是怎么发明的?

深度学习的发展是多位研究者长期探索的结果,其核心方法的形成并非由单一人物 “发明”,而是历经数十年理论积累与技术突破的产物。以下从关键人物、核心技术突破及历史背景三个维度,梳理深度学习方法的起源与发展脉络: 一、深度学习的奠基者与关键贡献者 1. Geoffrey Hin…

Jmeter ServerAgent在arm环境启动报错no libsigar-aarch64-linux.so in java.library.path

使用Jmeter压测的时候&#xff0c;用ServerAgent监测arm服务器的性能指标&#xff0c;在启动ServerAgent时&#xff0c;报错了&#xff1a;no libsigar-aarch64-linux.so in java.library.path 解决方案&#xff1a; 下载libsigar-aarch64-linux.so文件&#xff0c;放置在Serv…

AJAX拦截器失效排查指南:当你的beforeSend有效但error/complete沉默时

问题现象 开发者常遇到这样的场景&#xff1a; $.ajaxSetup({beforeSend: () > console.log("✅ 触发"), // 正常执行error: () > console.log("❌ 未触发"), // 静默失效complete: () > console.log("⚡ 未触发") // 同样沉默 })…

【模型微调】负样本选择

1.核心设计理念 非对称检索任务&#xff08;例如&#xff0c;用一个简短的问题去文档库里查找答案&#xff09;的一个核心挑战是查询&#xff08;query&#xff09;和文档&#xff08;passage&#xff09;在文本特征上的巨大差异。以往的研究发现&#xff0c;为查询和文档提供…

下载安装redis

有任何问题&#xff0c;都可以私信博主&#xff0c;共同探讨学习。 正文开始 一、下载安装redis一、启动redis总结 一、下载安装redis redis官方下载地址是github&#xff0c;有条件的同学可以自行搜索下载。针对部分网速不太好的同学&#xff0c;可以通过网盘获取&#xff0c…

flutter 项目配置Gradle下载代理

如图&#xff0c; 在Android Studio中配置代理是不生效的。 需要在flutter sdk的Gradle中去配置代理

世冠科技亮相TMC,以国产MBD工具链赋能汽车电控系统开发新未来

2025年6月12日至13日&#xff0c;第十七届国际汽车动力系统技术年会&#xff08;TMC2025&#xff09;在南通国际会展中心盛大召开。作为全球汽车动力系统领域规模最大、规格最高、内容最前沿的标杆性国际盛会&#xff0c;汇聚了来自全球整车企业、核心零部件供应商、顶尖科研机…

将本地项目与远程 Git 仓库关联的完整步骤

将本地项目与远程 Git 仓库关联的完整步骤 现在的情景是&#xff1a;本地文件项目已经写好了&#xff0c;亦或者远程仓库已经建好了&#xff0c;需要与本地项目关联起来 以下是详细的操作流程&#xff0c;我会用清晰的步骤说明如何将你的本地项目与远程 Git 仓库关联&#xf…

3DS 转换为 STP 全攻略:迪威模型网在线转换详解

在三维模型创作与应用的多元场景中&#xff0c;不同格式的文件承担着独特的角色。3DS&#xff08;3D Studio&#xff09;格式是 Autodesk 3ds Max 早期广泛使用的文件格式&#xff0c;常用于游戏开发、影视特效制作等领域&#xff0c;能够存储模型的几何形状、材质、动画等信息…