操作系统—自定义系统调用

news2024/12/23 14:35:26

文章目录

  • 自定义系统调用
    • 1.实验基本环境
      • (1).基本系统环境
      • (2).选择替换WSL内核的起因
      • (3).我尝试的改进措施
    • 2.添加系统调用
      • (1).系统调用位置
      • (2).系统调用函数编写
      • (3).添加系统调用号
      • (4).添加编译参数并编译
        • #1.一次极其失败的尝试
        • #2.推倒重来
      • (5).尝试调用sys_mysyscall
    • 3.后记
    • 参考文献
    • 附录I. 我实现的println函数

自定义系统调用

1.实验基本环境

(1).基本系统环境

  之前看了一会儿MIT的xv6的第一个lab,跟着做了两个用户态程序(pingpong和sleep),不过我还是不太熟悉xv6本身的系统调用,因此在这里首先尝试对Linux内核进行自定义系统调用的操作,这里我采用的环境如下:

  • 系统:Windows Subsystem for Linux(WSL 2) Ubuntu-22.04 LTS
  • 编译时Linux内核:Linux-5.15.133.1-microsoft-standard-WSL2
  • 被替换Linux内核:Linux-6.1.21.2-microsoft-standard-WSL2

(2).选择替换WSL内核的起因

  这个Lab中我决定尝试在自定义完系统调用后,编译并替换掉wsl中的内核,我之前同上个学期修石亮老师操作系统的一个同学聊过,他在完成这个实验尝试替换wsl内核的时候遇到了障碍,我后来了解他的步骤之后发现可能问题出在内核代码本身:wsl内核的代码和kernel.org下的linux原版代码不太一致,微软对其进行了一定程度的定制

(3).我尝试的改进措施

  在完成这个实验的时候,我猜想他实验失败可能问题就出在这里,因此我在下载内核源码时直接从微软的WSL2-Linux-Kernel仓库中下载源码,并且因为考虑到我本机的Linux内核已经到了Linux 5.15;并且Linus对Rust持支持态度,在Linux 6系列的内核当中引入了很多由Rust实现的代码,所以这次实验中,我决定将内核替换为6.1.21.2。当然,我不是Rust的支持者,我只是想试一下新版本的内核罢了,所以6.1.21.2版本的内核代码在仓库的Release中可以直接找到:

p14

  从仓库下载代码后,在wsl终端内输入:

tar -xzf WSL2-Linux-Kernel-linux-msft-wsl-6.1.21.2.tar.gz

  这样就完成了解压操作,接下来就是具体的代码修改以及编译、替换工作了

2.添加系统调用

  一上来就修改内核,我其实还是有点害怕的,要是我代码有点什么问题,到时候我的wsl会不会出什么问题,就不得而知了

(1).系统调用位置

  Linux内核的所有syscall都被定义在./kernel/sys.c当中,我们打开sys.c可以看到如211行中定义的SYSCALL_DEFINE3:

SYSCALL_DEFINE3(setpriority, int, which, int, who, int, niceval)
{
	struct task_struct *g, *p;
	struct user_struct *user;
	const struct cred *cred = current_cred();
	int error = -EINVAL;
  ...

  所以我们后续要定义自己的系统调用,也需要再sys.c当中完成对应的函数,不过有个问题,这个SYSCALL_DEFINE3看起来并不是很直观,它和我们平时见到的C语言函数定义不太一样,因此我觉得,这肯定是一个宏定义,在我详细地阅读了sys.c文件的包含目录后,发现了一行代码:

#include <linux/syscalls.h>

  这个名字,听起来就和我想知道的系统调用内容非常相符,因此我在./include/linux下找到了这个syscalls.h,并且找到了这个SYSCALL_DEFINEx对应的宏定义:

#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)

#define SYSCALL_DEFINE_MAXARGS	6

#define SYSCALL_DEFINEx(x, sname, ...)				\
	SYSCALL_METADATA(sname, x, __VA_ARGS__)			\
	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

  分析一下这一串代码,SYSCALL_DEFINE1~6的实现都是基于SYSCALL_DEFINEx这个宏定义完成的,而SYSCALL_DEFINEx是一个利用可变参数实现的宏,其中的x实际上是后续可变参数的数量,它的出现应当是为了后续我们完成一系列自定义的系统调用中,参数的数量是可以自定的,之前我也曾经基于stdarg.h和fprintf实现了一个小的println函数,因为当时比较懒,就只做了format字符串解析和可变参数的部分,还没有完全实现像屏幕输出的操作,具体的代码我附在最后了,可以参考一下。

  回到关键任务上,我们发现,SYSCALL_DEFINEx这个宏还有两个调用的宏定义,一个是 __SYSCALL_DEFINEx,一个是SYSCALL_METADATA,所以我们可以在下面先找到 __SYSCALL_DEFINEx的定义:

#ifndef __SYSCALL_DEFINEx
#define __SYSCALL_DEFINEx(x, name, ...)					\
	__diag_push();							\
	__diag_ignore(GCC, 8, "-Wattribute-alias",			\
		      "Type aliasing is used to sanitize syscall arguments");\
	asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))	\
		__attribute__((alias(__stringify(__se_sys##name))));	\
	ALLOW_ERROR_INJECTION(sys##name, ERRNO);			\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));	\
	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))	\
	{								\
		long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
		__MAP(x,__SC_TEST,__VA_ARGS__);				\
		__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));	\
		return ret;						\
	}								\
	__diag_pop();							\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))
#endif /* __SYSCALL_DEFINEx */

  我尝试读了一下,然后发现,我好像真的读不懂,虽然里面一些什么__VA_ARGS__这样的熟悉面孔,但是整体的宏定义太多,难度过大,于是我交给了ChatGPT帮我解释这段代码,我总结了一下它的意思,它说:这个宏的目的是生成系统调用函数的相关代码,首先将当前诊断状态推入栈中,以便后续诊断,之后忽略GCC的警告8类型的-Wattribute-alias,即属性别名,之后还定义了一个sys##name函数,这里的##用到了C语言宏定义的连接机制,例如:

#define CONCAT(a, b) a##b

  经过预编译后,CONCAT(aaa, bbb)会被展开为aaabbb,所以对于给定的name参数,假设name为mysyscall,那么这个函数会负责生成一个sysmysyscall的函数,之后调用了__attribute__(())这个在gcc编译器对C语言做的扩展,这个扩展可以允许我们对编译过程提供额外信息,控制编译器行为,这个其实已经用到了,在第一次环境配置的时候,部分同学在最后一步make xv6内核的时候,出现了以下的问题:

p2

  事实上就是编译器在此检测到了可能发生的无限递归,这个问题本来是不该出现的,但貌似是因为一些编译参数而导致了这个问题,所以我们需要在这里通过__attribute__(())的方式告诉编译器,runcmd函数明确不存在返回值,也就是改为下图的情况:

p2

  这样一来,重新make就不会再报错了。好了,回到内核,之后它允许对sys##name进行错误注入(这一步我没有看懂),之后的静态内联函数__do_sys##name比较关键,它将当初传入的可变参数传入,并且进行了对应的展开操作。

	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));	\
	asmlinkage long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))	\
	{								\
		long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
		__MAP(x,__SC_TEST,__VA_ARGS__);				\
		__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));	\
		return ret;						\
	}								\

  之后定义__se_sys##name函数,根据参数展开后,完成真正的系统调用函数的调用,首先执行系统调用,获得返回值,赋值到ret;然后对传入参数进行测试,然后对参数和返回值进行保护操作(这里我也不知道保护是干啥的),然后再返回系统调用的返回值,这样一整个操作就结束了,__SYSCALL_DEFINEx完成了对于某一个系统调用的函数生成,以我的理解,作为内核态的程序,代码如果过于随意,实际上可能会导致很多问题,因此需要一个比较完善的保护机制来完成代码的生成,而__SYSCALL_DEFINEx完成的就是这个过程。

  然后我又找到了SYSCALL_METADATA,这是它的定义:

#define SYSCALL_METADATA(sname, nb, ...)			\
	static const char *types_##sname[] = {			\
		__MAP(nb,__SC_STR_TDECL,__VA_ARGS__)		\
	};							\
	static const char *args_##sname[] = {			\
		__MAP(nb,__SC_STR_ADECL,__VA_ARGS__)		\
	};							\
	SYSCALL_TRACE_ENTER_EVENT(sname);			\
	SYSCALL_TRACE_EXIT_EVENT(sname);			\
	static struct syscall_metadata __used			\
	  __syscall_meta_##sname = {				\
		.name 		= "sys"#sname,			\
		.syscall_nr	= -1,	/* Filled in at boot */	\
		.nb_args 	= nb,				\
		.types		= nb ? types_##sname : NULL,	\
		.args		= nb ? args_##sname : NULL,	\
		.enter_event	= &event_enter_##sname,		\
		.exit_event	= &event_exit_##sname,		\
		.enter_fields	= LIST_HEAD_INIT(__syscall_meta_##sname.enter_fields), \
	};							\
	static struct syscall_metadata __used			\
	  __section("__syscalls_metadata")			\
	 *__p_syscall_meta_##sname = &__syscall_meta_##sname;

  这一段代码用于定义系统调用的元数据信息,大概目的是用于系统调用的一些跟踪,也有可能会产生其它的用途,这里不再赘述。

  虽然上面的代码很复杂,但是实际上编写一个系统调用函数没有很复杂,只要写完函数之后再使用对应的宏即可。

(2).系统调用函数编写

  所以我们在sys.c中加入以下代码:

asmlinkage long sys_mysyscall(long num)
{
    printk("This is Voltline's syscall!\n");
    printk("I think %ld is a good number!\n", num);
    return 0;   
}

SYSCALL_DEFINE1(mysyscall, long, num)
{
    return sys_mysyscall(num);
}
  • sys_mysyscall是我的系统调用函数,我在其中使用了printk这个内核打印消息的函数打印了两条信息
  • 然后使用了SYSCALL_DEFINE1完成单一参数的系统调用代码生成
  • asmlinkage要求函数采用和内核调用约定相匹配的参数传递方式

(3).添加系统调用号

  为了方便后续调用我自己的系统调用,我还需要给内核添加这个系统调用对应的系统调用号,查阅资料之后发现我应该去./arch/x86/entry/syscalls下的syscall_64.tbl中添加一个系统调用号:

cd arch/x86/entry/syscalls

  翻到最后的最后,看到了下面的调用号表:

448	common	process_mrelease	sys_process_mrelease
449	common	futex_waitv		sys_futex_waitv
450	common	set_mempolicy_home_node	sys_set_mempolicy_home_node

#
# Due to a historical design error, certain syscalls are numbered differently
# in x32 as compared to native x86_64.  These syscalls have numbers 512-547.
# Do not add new syscalls to this range.  Numbers 548 and above are available
# for non-x32 use.
#

  下面的512-547它不让我改,所以我就在450后加上一个451号的系统调用:

450	64		mysyscall		sys_mysyscall

  然后要去./include/linux下的syscalls.h中加入我的sys_mysyscall的函数声明:

asmlinkage long sys_mysyscall(long num);

  编辑后的结果:

p3

(4).添加编译参数并编译

#1.一次极其失败的尝试

请注意,下面的一部分,全部都是错误操作!请不要跟着操作!

请注意,下面的一部分,全部都是错误操作!请不要跟着操作!

请注意,下面的一部分,全部都是错误操作!请不要跟着操作!

  接下来就要编译内核了!首先清理编译信息:

make mrproper

  安装必要的库:

sudo apt-get install libncurses-dev flex bison libelf-dev libssl-dev dwarves

  然后配置.config文件:

make menuconfig

p4

  完成.config文件的配置,保存并退出,然后开始make:

make -j16

  非常顺利的在我还没开始take a seat and relax的时候,就报错了,非常好:

p5

  看了看报错信息,回去看了以下syscalls_64.tbl,发现自己复制450号系统调用下来之后忘记把调用号改成451了,然后改回去了
p6

  然后就重新配置,然后重新make -j16开始编译,这下看起来比较顺畅,可以take a seat and relax啦!

  我还真以为就结束了,结果说完这句话就error了:

p8

  在Google之后发现有人在kernel.org上反映了这个问题:tg3: fix array subscript out of bounds compilation error (kernel.org),于是我又对这个tg3.c修改了一下:

>> -	for (i = 0; i < tp->irq_max; i++) {
>> +	for (i = 0; i < tp->irq_max && i < TG3_IRQ_MAX_VECS; i++) {

p9

  内核的编译果然很严格哈,修改完了之后我又开始make -j16,结果没多久又出问题了,这次是在block/blk-iocost.c下的两个seq_printf的format字符串中有两个和后续打印内容的数据类型不匹配的,我也在[PATCH] block/blk-iocost (gcc13): cast enum members to int in prints (kernel.org)找到了解答,修改了如下的内容:

p10
  这一次,编译通过啦!
p11
  接下来在arch/x86/boot下找到bzImage,这就是内核文件了,拷贝出来更名为kernel,然后在powershell中输入

wsl --shutdown
#2.推倒重来

  关闭WSL,然后去替换kernel,结果发现,我的wsl再也打不开了,我开始非常迷茫、混乱、崩溃,在查询了各种资料之后发现:我的问题好像根本没有人遇到过,于是我决定重新解压一遍压缩包,然后找到了另一篇资料,发现它的make只需要用到下面一行指令:

sudo make KCONFIG_CONFIG=Microsoft/config-wsl -j16

  当然,首先还是需要安装编译内核需要的依赖:

sudo apt-get install libncurses-dev flex bison libelf-dev libssl-dev dwarves

  然后我重写了一次系统调用,然后使用上面的命令重新编译,完成替换,最后的最后:
p12
  泪目了,内核替换成功了,以前喜欢给手机刷机的时候,大家说:不要在晚上八点之后给手机刷机,现在我也想说:不要在晚上八点之后尝试编译大型项目,内核的替换成功了,这也就说明,我们的代码应该基本没有问题了,接下来就可以尝试在用户态调用系统调用了。

(5).尝试调用sys_mysyscall

  接下来我在~/Document/syscall_test下创建了syscall_test.c文件,写入了下面的代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>

int main()
{
    syscall(451, 20240307);
}

  然后编译运行程序,之后使用dmesg查看系统调用信息:

gcc syscall_test.c -o syscall_test 
./syscall_test
dmesg

  最后的最后,我们可以看到:

p13

  调用信息里出现了我们刚刚写的系统调用会打印的两条信息,并且它也觉得20240307是个很好的数字,至此,自定义的sys_mysyscall已经顺利完成。

3.后记

  说实话,这个实验不太难,但是的确出现了很多问题,其实我早在一年之前就已经通过微软的WSL内核仓库更新过WSL内核,当时是跟着一篇教程完成的,非常顺利,过程中一个ERROR都没有出现过,而这一次就显得磕磕绊绊的,甚至中间还出了个大错误,不过这倒的确是个有意思的过程:我真的在我经常用到的系统内核里加入了自己的代码,成就感相当足啊。

参考文献

    1. 操作系统概述 (操作系统的历史;学习建议) [南京大学2023操作系统-P1] (蒋炎岩)
    1. WSL2编译内核并更改替换内核版本_linux-msft-wsl-5.15.123.1+linux-msft-wsl-6.1.21.2-CSDN博客⁤
    1. Advanced settings configuration in WSL | Microsoft Learn
    1. 为linux添加一个系统调用_利用内核模块法为linux添加一个系统调用-CSDN博客

附录I. 我实现的println函数

  println函数的行为与printf函数基本一致,返回值为通过format字符串打印参数的数量,可以接受一个format字符串以及对应的各种参数,在下面的实现当中,我利用有限自动机和va_list实现了对应的解析和打印操作,不过最后的输出还是简单地调用了fprintf,以后我一定会改进的(如果在网上看到这段代码的话,应该也是我写的,毕竟一般会发到博客上的都是实现得比较完整的代码):

int println(const char* format, ...)
{
	const char* p = format;
	va_list ptr;
	va_start(ptr, format);

	int state{ 0 };
	int _sum{ 0 };
	char kf[15] = "%.1";
	char pt{ 2 };
	// 0 for default char
	// 1 for %  just %
	// 2 for .(.kf) 
	// 3 for l(lf, ld, lu)
	// 4 for ll(lld, llu, llf=lf)
	// 5 for k(.kf)
	for (; *p; p++) {
		switch (*p) {
		case '%':
			if (state) {
				state = 0, putchar(*p);
			}
			else state = 1;
			break;
		case 'l':
			if (state) {
				if (state == 1) state = 3;
				else if (state == 3) state = 4;
				else if (state == 4) {
					return -1;
				}
			}
			else putchar(*p), state = 0;
			break;
		case '.':
			if (state == 1) {
				state = 2;
			}
			else putchar(*p), state = 0;
			break;
		case '0': case '1': case '2': case '3': case '4': 
		case '5': case '6': case '7': case '8': case '9':
			if (state) {
				if (state == 2) {
					state = 5;
				}
				kf[pt++] = *p;
			}
			else putchar(*p), state = 0;
			break;
		case 'd':
			if (state) {
				switch (state) {
				case 1:
					fprintf(stdout, "%d", va_arg(ptr, int));
					break;
				case 3:
					fprintf(stdout, "%ld", va_arg(ptr, long));
					break;
				case 4:
					fprintf(stdout, "%lld", va_arg(ptr, long long));
					break;
				}
				_sum++, state = 0;
			}
			else  putchar(*p), state = 0;
			break;
		case 'u':
			if (state) {
				switch (state) {
				case 1:
					fprintf(stdout, "%u", va_arg(ptr, unsigned int));
					break;
				case 3:
					fprintf(stdout, "%lu", va_arg(ptr, unsigned long));
					break;
				case 4:
					fprintf(stdout, "%llu", va_arg(ptr, unsigned long long));
					break;
				}
				_sum++, state = 0;
			}
			else  putchar(*p), state = 0;
			break;
		case 'c':
			if (state == 1) {
				putchar(va_arg(ptr, char));
				_sum++, state = 0;
			}
			else  putchar(*p), state = 0;
			break;
		case 'f':
			if (state) {
				switch (state) {
				case 1:
					fprintf(stdout, "%f", va_arg(ptr, float));
					break;
				case 3:
					fprintf(stdout, "%lf", va_arg(ptr, double));
					break;
				case 4:
					fprintf(stdout, "%lf", va_arg(ptr, double));
					break;
				case 2: case 5:
					kf[pt++] = 'f';
					kf[pt] = 0;
					fprintf(stdout, kf, va_arg(ptr, double));
					break;
				}
				_sum++, state = 0;
			}
			else putchar(*p), state = 0;
			break;
		case 's':
			if (state == 1) {
				const char* str = va_arg(ptr, const char*);
				while (*str) {
					putchar(*str++);
				}
				_sum++, state = 0;
			}
			else putchar(*p), state = 0;
			break;
		default:
			putchar(*p);
			if (state) state = 0;
		}
	}
	putchar('\n');
	va_end(ptr);
	return _sum;
}

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

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

相关文章

[译]Python 和 TOML:新最好的朋友 (2) 使用Python操作TOML

文章目录 Python 和 TOML&#xff1a;新最好的朋友使用Python加载TOML使用tomli 或 tomllib 读取TOML文档比较TOML类型和Python类型在项目中使用配置文件 将Python对象转换为TOML将字典转换为 TOML通过tomli_w 写TOML文档 创建新的TOML文件格式和样式用tomlkit 从头开始创建 TO…

创建springboot 2.x web空项目(IDEA)

由于学习时候发现spring官网只能创建springboot3.0的项目&#xff0c;而且不支持java1.8&#xff0c;无法选择java8作为java版本&#xff0c;导致很多教程无法跟着做&#xff0c;因此记录一下可行的创建过程。 &#xff08;Tips:当前spring Initializr不支持java8的解决方式&a…

在win11关闭copilot

在设置中的个性化设置&#xff0c;任务栏处关闭即可。 参考资料 How to disable Copilot in Windows 11

6种最佳的UI设计工具!

最好的UI设计工具可以适应几乎每一个设计过程&#xff0c;并有望满足您的创意需求。UI设计工具为设计师提供了高保真线框图、物理模型和原型所需的精确设计&#xff0c;并提供了最小限度的可行性产品。它们代表设计细节&#xff0c;传达设计功能。用户体验设计工具关注用户&…

C++作业day2

封装一个矩形类(Rect)&#xff0c;拥有私有属性:宽度(width)、高度(height)&#xff0c; 定义公有成员函数: 初始化函数:void init(int w, int h) 更改宽度的函数:set_w(int w) 更改高度的函数:set_h(int h) 输出该矩形的周长和面积函数:void show() #include <iostre…

BM1684X搭建sophon c++环境

1:首先安装编译好sophon-sail 比特大陆BM1684X开发环境搭建--SOC mode-CSDN博客 2:在将之前配置的soc-sdk拷贝一份到sdk根目录&#xff0c;将交叉编译好的sail中的build_soc拷贝至soc-sdk文件夹内&#xff1b; cp -rf build_soc/sophon-sail/inlcude soc-sdk cp -rf build_soc…

测试用例实战(全网最详细-注册,登录,发布文章)

1.注册测试用例实战 需求图片如下&#xff1a; 注册测试点&#xff08;用等价类和边界值&#xff09; 重点提示&#xff1a; 正向&#xff1a;⼀次尽量覆盖多条 逆向&#xff1a;⼀次只能覆盖⼀条&#xff0c;其他选项必须正确。 注册测试用例 注意&#xff1a;注册的测试点…

Linux:深入文件系统

一、Inode 我们使用ls -l的时候看到的除了看到文件名&#xff0c;还看到了文件元数据。 [rootlocalhost linux]# ls -l 总用量 12 -rwxr-xr-x. 1 root root 7438 "9月 13 14:56" a.out -rw-r--r--. 1 root root 654 "9月 13 14:56" test.c 每行包含7列&…

一文弄懂空间金字塔池化网络

目录 空间金字塔池化网络是什么&#xff1f; 组成部分和工作原理 具体步骤 ​编辑 主要优点 空间金字塔池化网络是什么&#xff1f; 空间金字塔池化网络&#xff08;Spatial Pyramid Pooling Network&#xff0c;SPPNet&#xff09;是一种用于处理具有不同尺寸和比例的输入…

选股就用河北源达“财源滚滚”选股软件

在股市投资的道路上&#xff0c;选股无疑是至关重要的一环。然而&#xff0c;面对海量的个股信息和复杂的市场环境&#xff0c;如何科学、准确地选股&#xff0c;成为了投资者必须面对的难题。河北源达信息技术股份有限公司推出的“财源滚滚”选股软件&#xff0c;以其独特的优…

Kotlin 数据解析(Gson)

一、添加依赖 build.gradle.kts(:app) // gson数据解析implementation("com.google.code.gson:gson:2.8.6") 对象类&#xff1a; // 对象类 class Account {var uid:String "00001"var userName:String "Freeman"var password:String &quo…

使用IDEA构建SpringBoot程序的镜像

实战&#xff1a;使用IDEA构建SpringBoot程序的镜像 这里就以我之前写的WIT问卷管理系统为例子吧。 首先在之前写好的SpringBoot项目中新建一个DockerFile 在Dockerfile中写入我们的base镜像&#xff0c;之前我已经创建好了并且传到docker仓库了&#xff0c;这里就直接拉取 …

【教程】APP加固的那些小事情

摘要 APP加固是保护APP代码逻辑的重要手段&#xff0c;通过隐藏、混淆、加密等操作提高软件的逆向成本&#xff0c;降低被破解的几率&#xff0c;保障开发者和用户利益。本文将介绍APP加固常见失败原因及解决方法&#xff0c;以及处理安装出现问题的情况和资源文件加固策略选择…

yolo发展历史

yolo系列 一、任务描述二、设计思想三、发展历程1. YOLOv11.1问题背景1.2创新点1.3训练流程1.4检测流程1.4.1 优点1.4.2 缺点 2. YOLOv22.1问题背景2.2创新点2.3提升性能的方法2.4训练流程&#xff1f;&#xff1f;&#xff1f;&#xff1f; 3. YOLOv33.1问题背景3.2模型改进 4…

MySQL学习Day31-主从复制

一、主从复制概述: 主从复制作用1:读写分离&#xff0c;可以通过主从复制的方式来同步数据&#xff0c;然后通过读写分离提高数据库并发处理能力;其中一个是Master主库&#xff0c;负责写入数据;其它都是slave从库&#xff0c;负责读取数据。当主库进行更新的时候&#xff0c;…

相机安装位置固定后开始调试设备供电公司推荐使用方法

摄像头安装位置固定后开始调试 设备供电&#xff1a;无电源设备需要连接12V/2A电源并连接到摄像机的DC端口&#xff0c;而有电源的摄像机可以直接连接到220V电源。 连接设备&#xff1a;如果是有线连接&#xff0c;请使用网线将设备连接到电脑&#xff08;建议直接连接&#…

Text-to-SQL 工具Vanna + MySQL本地部署 | 数据库对话机器人

今天我们来重点研究与实测一个开源的Text2SQL优化框架 – Vanna 1. Vanna 简介【Text-to-SQL 工具】 Vanna 是一个基于 MIT 许可的开源 Python RAG&#xff08;检索增强生成&#xff09;框架&#xff0c;用于 SQL 生成和相关功能。它允许用户在数据上训练一个 RAG “模型”&a…

PLC数采网关在实际应用中的效能-天拓四方

在工业自动化领域中&#xff0c;PLC扮演着至关重要的角色&#xff0c;它负责控制和监测生产线的各个环节。然而&#xff0c;随着工业4.0的推进和智能制造的快速发展&#xff0c;单纯依靠PLC进行现场控制已无法满足企业对数据集中管理、远程监控和智能分析的需求。因此&#xff…

立即注册 | 线上讲座:借助 NGINX Plus 优化物联网数据传输和应用安全

原文作者&#xff1a;NGINX 原文链接&#xff1a;立即注册 | 线上讲座&#xff1a;借助 NGINX Plus 优化物联网数据传输和应用安全 转载来源&#xff1a;NGINX 开源社区 NGINX 唯一中文官方社区 &#xff0c;尽在 nginx.org.cn 基本信息 课程主题 借助 NGINX Plus 优化物联网…

地理数据 vs. 3D数据

在表示我们周围的物理世界时&#xff0c;地理空间数据和 3D 建筑数据是两个最常见的选择。 他们在各个行业和项目中发挥着至关重要的作用。 从构建数字孪生到可视化城市景观和创建沉浸式应用程序。 尽管地理空间和 3D 建筑数据有相似之处&#xff0c;但它们不可互换。 虽然地…