从0开始的操作系统手搓教程45——实现exec
目录
建立抽象
实现加载
实现sys_execv
!!!提示:因为实现问题没有测试。所以更像是笔记!
exec
函数的作用是用新的可执行文件替换当前进程的程序体。具体来说,exec
会将当前正在运行的用户进程的进程体(包括代码段、数据段、堆、栈等)替换为一个新的可执行文件的进程体。这样,新的程序会接管当前进程的地址空间,继续执行新程序的代码,但该进程的 PID(进程ID)保持不变。也就是说,执行 exec
后,原来进程的地址空间被清除,并且新程序的内容会加载到同样的进程中,继续执行。
为什么需要实现 exec
呢?这个问题的答案与 shell 的工作方式密切相关。在实现一些简单的命令时,我们使用了类似 if-else if
的结构来判断并执行不同的命令。然而,这种方法存在很大的局限性。首先,它无法处理用户输入的新命令,因为我们不能预见到用户会输入什么命令,且每添加一个新命令就需要修改代码并重新编译。这种方式不仅繁琐,而且无法应对外部程序的运行。
exec
的实现解决了这个问题。当 exec
被调用时,它允许用户运行外部程序,而不需要修改 shell 本身的代码。用户输入的命令会被解析,且通过 exec
函数加载并执行对应的外部程序,从而提供了更灵活的命令执行方式。
exec
是一个函数簇,包含多个相关的函数,区别主要在于如何表示程序对象以及是否传入环境变量。例如,execv
函数就不需要传入环境变量,但其他 exec
函数可能会接受额外的环境变量。
当调用 execv
时,如果执行成功,进程将直接跳转到新程序,并不会返回,因此它没有返回值。调用 execv
失败时,它会返回 -1
,并设置错误码。这是因为 exec
执行新程序时,原进程的执行流被完全替换,进程不会再回到原来的位置,因而不需要像传统函数那样返回值。
建立抽象
我们先对exe文件做抽象:
extern void intr_exit(void);
typedef uint32_t Elf32_Word, Elf32_Addr, Elf32_Off;
typedef uint16_t Elf32_Half;
/* 32-bit ELF header */
struct Elf32_Ehdr {unsigned char e_ident[16]; // ELF identification bytesElf32_Half e_type; // Type of file (e.g., executable)Elf32_Half e_machine; // Machine architectureElf32_Word e_version; // ELF versionElf32_Addr e_entry; // Entry point addressElf32_Off e_phoff; // Program header offsetElf32_Off e_shoff; // Section header offsetElf32_Word e_flags; // Processor-specific flagsElf32_Half e_ehsize; // ELF header sizeElf32_Half e_phentsize; // Program header entry sizeElf32_Half e_phnum; // Number of program headersElf32_Half e_shentsize; // Section header entry sizeElf32_Half e_shnum; // Number of section headersElf32_Half e_shstrndx; // Section header string table index
};
/* Program header (segment descriptor) */
struct Elf32_Phdr {Elf32_Word p_type; // Segment type (e.g., PT_LOAD)Elf32_Off p_offset; // Offset in fileElf32_Addr p_vaddr; // Virtual address in memoryElf32_Addr p_paddr; // Physical address (unused)Elf32_Word p_filesz; // Size of segment in fileElf32_Word p_memsz; // Size of segment in memoryElf32_Word p_flags; // Segment flagsElf32_Word p_align; // Segment alignment
};
/* Segment types */
enum segment_type {PT_NULL, // Ignore segmentPT_LOAD, // Loadable segmentPT_DYNAMIC, // Dynamic loading informationPT_INTERP, // Name of dynamic loaderPT_NOTE, // Auxiliary informationPT_SHLIB, // ReservedPT_PHDR // Program header
};
这段代码定义了32位ELF(Executable and Linkable Format)格式的结构体以及相关的常量,用于描述ELF文件的头部和程序段的描述。具体来说,主要包括以下内容:
-
Elf32_Ehdr: 该结构体表示ELF文件的头部,包含了ELF文件的基本信息,如文件标识、类型、机器架构、入口地址、程序头的偏移量等。具体字段的含义如下:
-
e_ident
:ELF文件标识字节,用于标识文件类型和版本。 -
e_type
:文件类型,表明ELF文件是可执行文件、共享库文件还是其他类型。 -
e_machine
:表示机器架构的字段,如x86、ARM等。 -
e_version
:ELF版本,通常为1。 -
e_entry
:程序入口点的地址。 -
e_phoff
:程序头部的偏移量,指向包含程序段信息的位置。 -
e_shoff
:节头部的偏移量,指向包含节信息的位置。 -
e_flags
:处理器特定的标志。 -
e_ehsize
:ELF头部的大小。 -
e_phentsize
:程序头项的大小。 -
e_phnum
:程序头的数量。 -
e_shentsize
:节头项的大小。 -
e_shnum
:节头的数量。 -
e_shstrndx
:节头字符串表的索引。
-
-
Elf32_Phdr: 该结构体表示ELF文件中的程序头(segment descriptor),用于描述文件中的每个段。字段的含义如下:
-
p_type
:段的类型,如可加载段、动态段等。 -
p_offset
:段在文件中的偏移。 -
p_vaddr
:段在内存中的虚拟地址。 -
p_paddr
:段在物理内存中的地址(通常不使用)。 -
p_filesz
:段在文件中的大小。 -
p_memsz
:段在内存中的大小。 -
p_flags
:段的标志,如可读、可写、可执行等。 -
p_align
:段的对齐方式。
-
-
segment_type:该枚举定义了常见的段类型,如:
-
PT_NULL
:表示忽略该段。 -
PT_LOAD
:表示可加载的段(常见的代码和数据段)。 -
PT_DYNAMIC
:动态加载信息。 -
PT_INTERP
:动态加载器的名称。 -
PT_NOTE
:辅助信息。 -
PT_SHLIB
:保留段。 -
PT_PHDR
:程序头。
-
实现加载
/* Load a segment from a file into virtual memory at the specified address */
static bool segment_load(int32_t fd, uint32_t offset, uint32_t filesz,uint32_t vaddr) {uint32_t vaddr_first_page =vaddr & 0xfffff000; // First page of the virtual addressuint32_t size_in_first_page =PG_SIZE - (vaddr & 0x00000fff); // Size of the segment in the first pageuint32_t occupy_pages = 0;
// If the segment doesn't fit in a single pageif (filesz > size_in_first_page) {uint32_t left_size = filesz - size_in_first_page;occupy_pages = ROUNDUP(left_size, PG_SIZE) + 1; // +1 for the first page} else {occupy_pages = 1;}
// Allocate memory for the segment in the process's address spaceuint32_t page_idx = 0;uint32_t vaddr_page = vaddr_first_page;while (page_idx < occupy_pages) {uint32_t *pde = pde_ptr(vaddr_page); // Page directory entryuint32_t *pte = pte_ptr(vaddr_page); // Page table entry
// Allocate memory if PDE or PTE doesn't existif (!(*pde & PG_P_1) || !(*pte & PG_P_1)) {if (!get_a_page(PF_USER, vaddr_page)) {return false;}}vaddr_page += PG_SIZE;page_idx++;}
// Read the segment data from the file and load it into memorysys_lseek(fd, offset, SEEK_SET);sys_read(fd, (void *)vaddr, filesz);return true;
}
函数 segment_load
负责将一个可执行文件中的特定段加载到进程的虚拟内存中,它接收四个参数:文件描述符 fd
,段在文件中的偏移量 offset
,段大小 filesz
,以及段应加载到的虚拟地址 vaddr
。其中 filesz
命名虽然让人容易联想到整个文件大小,但它其实是 ELF 格式中段头部的字段名 p_filesz
,表示当前这个段在文件中的实际大小,因此用作参数名是为了与 ELF 中的结构保持一致。
段的加载实质上就是内核为新进程分配内存的过程。由于程序通常由多个段组成,内核需要对每个段逐一加载。加载时以页为单位进行内存管理,因此即使一个段不满一页,也必须以页为粒度分配内存。变量 vaddr_first_page
是将段的虚拟地址 vaddr
向下对齐到页起始地址,用于确定从哪里开始分配页框。而变量 size_in_first_page
则表示该段在第一页中所占用的字节数,如果 filesz
大于这个值,说明段会跨页,因此接下来计算还需多少页框,最终由 occupy_pages
给出总的页框数。
接下来是页框分配逻辑,考虑到这是 exec 执行新程序的场景,当前进程的页表结构还在用,若某虚拟地址已经存在对应的物理页,则无需重新分配,只需直接复用原页框覆盖其内容即可;否则就通过 get_a_page
分配一个新页框。分配时逐页判断并处理,直到整段的地址空间都被准备好。
页框分配完成后,便可以真正加载段的数据了。首先使用 sys_lseek
将文件读指针移动到段的起始偏移位置 offset
,再用 sys_read
将长度为 filesz
的数据读入到从 vaddr
开始的虚拟地址中。至此,这个段被完整加载进内存。整个过程体现了分段加载、按页管理、懒分配页框的设计思路,也保证了内存使用的灵活性与效率。
/* Load a user program from the filesystem by pathname, return entry point* address or -1 on failure */
static int32_t load(const char *pathname) {int32_t ret = -1;struct Elf32_Ehdr elf_header;struct Elf32_Phdr prog_header;k_memset(&elf_header, 0, sizeof(struct Elf32_Ehdr));
int32_t fd = sys_open(pathname, O_RDONLY); // Open the program fileif (fd == -1) {return -1;}
// Read the ELF header from the fileif (sys_read(fd, &elf_header, sizeof(struct Elf32_Ehdr)) !=sizeof(struct Elf32_Ehdr)) {ret = -1;goto done;}
// Verify the ELF headerif (k_memcmp(elf_header.e_ident, "\177ELF\1\1\1", 7) ||elf_header.e_type != 2 || elf_header.e_machine != 3 ||elf_header.e_version != 1 || elf_header.e_phnum > 1024 ||elf_header.e_phentsize != sizeof(struct Elf32_Phdr)) {ret = -1;goto done;}
Elf32_Off prog_header_offset = elf_header.e_phoff;Elf32_Half prog_header_size = elf_header.e_phentsize;
// Iterate over all program headersuint32_t prog_idx = 0;while (prog_idx < elf_header.e_phnum) {k_memset(&prog_header, 0, prog_header_size);
// Seek to the program header location in the filesys_lseek(fd, prog_header_offset, SEEK_SET);
// Read the program header from the fileif (sys_read(fd, &prog_header, prog_header_size) != prog_header_size) {ret = -1;goto done;}
// If the segment is loadable, load it into memoryif (PT_LOAD == prog_header.p_type) {if (!segment_load(fd, prog_header.p_offset, prog_header.p_filesz,prog_header.p_vaddr)) {ret = -1;goto done;}}
// Move to the next program headerprog_header_offset += elf_header.e_phentsize;prog_idx++;}
ret = elf_header.e_entry; // Return the entry point of the program
done:sys_close(fd); // Close the filereturn ret;
}
函数 load
的核心功能是加载一个 ELF 格式的用户程序文件,并将其段映射到当前进程的虚拟地址空间中。如果加载成功,返回值是该程序的入口地址(即进程执行的起点);如果失败,返回 −1。
函数开始先声明两个结构体变量:elf_header
和 prog_header
,分别用于保存 ELF 文件头和程序段头。在读取 ELF 文件头后(第 102 行),程序紧接着从第 108 行开始验证 ELF 文件是否合法。
首先检查的是 ELF 文件的魔数 e_ident[0-6]
,这 7 个字节应依次为:
-
0x7F
(用八进制\177
表示) -
'E'
(0x45) -
'L'
(0x4C) -
'F'
(0x46) -
1
:32 位格式 -
1
:小端格式 -
1
:版本号
这几项是 ELF 文件的标准标志,如果不匹配,说明该文件不是合法的 ELF 可执行文件。接下来还会检查以下几个字段:
-
e_type
是否为ET_EXEC
(值为 2,代表可执行文件) -
e_machine
是否为EM_386
(值为 3,表示 x86 架构) -
e_version
是否为 1(当前 ELF 版本) -
e_phnum
(程序头数量)是否小于等于 1024 -
e_phentsize
(每个程序头条目的大小)是否等于sizeof(Elf32_Phdr)
这些检查都通过后,才认为这是一个有效的 ELF 可执行文件。
接下来,从 ELF 头中读取段头信息的起始偏移地址 e_phoff
,读取到变量 prog_header_offset
。段头条目的字节大小 e_phentsize
赋给 prog_header_size
,条目总数 e_phnum
用于控制接下来的循环。
然后从第 122 行进入循环,逐个读取每个段头。每次循环会先通过 sys_lseek
将文件指针跳到对应段头位置,然后通过 sys_read
读取一条段头到 prog_header
。第 136 行判断该段是否是 PT_LOAD
类型,也就是是否是可加载段。如果是,就调用 segment_load
,将该段的内容从文件加载到内存对应的虚拟地址。
所有段处理完毕后,从 ELF 头中提取程序入口地址 e_entry
赋给返回值 ret
,这表示程序开始执行的地址。
最后,无论是否加载成功,都会通过 sys_close
关闭打开的 ELF 文件,返回值为加载成功的入口地址或失败的 −1。
总体来说,load
函数的实现非常典型地体现了 ELF 格式的标准解析流程、段式加载方式、虚拟内存分配控制等关键内核概念,是内核启动用户进程的核心部分之一。
实现sys_execv
/* Replace the current process with the program at the specified path */
int32_t sys_execv(const char *path, const char *argv[]) {uint32_t argc = 0;while (argv[argc]) {argc++; // Count the number of arguments}
// Load the program and get its entry pointint32_t entry_point = load(path);if (entry_point == -1) { // If loading failed, return -1return -1;}
TaskStruct *cur = current_thread(); // Get the current running thread (process)k_memcpy(cur->name, path, TASK_NAME_ARRAY_SZ); // Update the process name
// Update the stack with the argumentsInterrupt_Stack *intr_0_stack =(Interrupt_Stack *)((uint32_t)cur + PG_SIZE - sizeof(Interrupt_Stack));intr_0_stack->ebx = (int32_t)argv;intr_0_stack->ecx = argc;intr_0_stack->eip = (void *)entry_point;intr_0_stack->esp = (void *)KERNEL_V_START; // Set stack pointer to the highest// user space address
// Jump to the entry point of the new processasm volatile("movl %0, %%esp; jmp intr_exit":: "g"(intr_0_stack): "memory");return 0;
}
sys_execv
函数的作用是将当前正在运行的进程替换为另一个可执行文件 path
所指定的程序,同时把参数数组 argv
一并传给新程序。这个过程不会返回,一旦成功,当前进程就“变成”了另一个程序。
首先,函数会遍历 argv
,统计参数个数并存入变量 argc
。接着调用 load(path)
试图加载用户程序,如果加载失败(返回 -1),函数立即返回 -1。若加载成功,程序的入口地址会被保存下来,作为后续执行的跳转目标。
之后,函数更新当前进程控制块中的 name
字段,使其反映正在执行的新程序名,这样在通过 ps 等工具查看时会显示为新程序的名字。
然后获取当前线程的内核栈顶地址。此时栈中存储的是旧进程的中断现场,但很快要把这些内容替换掉,准备启动新进程。函数将参数个数 argc
写入栈中保存的 ecx
寄存器位置,将参数数组 argv
的地址写入 ebx
寄存器位置。因为 ebx
通常用于保存基地址,而 ecx
常用于计数,这是一种传统习惯,也便于未来从运行库中取参数。接着将程序入口地址写入 eip
,用于后续跳转执行;再将用户栈指针 esp
初始化为 0xc0000000,即用户空间最高地址,以便新程序使用。
设置完成后,通过内联汇编将 esp 寄存器修改为新的内核栈地址,并跳转到 intr_exit。这个跳转操作会恢复栈中保存的所有寄存器状态,包括 eip、esp 和参数寄存器等,相当于“伪装”从中断中返回,从而进入新程序的执行流程。
因为这个过程是不可逆的,调用成功后不会返回到原来的函数中,所以 return 0 这一行永远不会执行,它的存在只是为了避免编译器报错。整段代码实现的是典型的 exec 功能,用一个新的程序完全替换当前进程的执行内容。
下一篇
从0开始的操作系统手搓教程46——实现wait和exit-CSDN博客文章浏览阅读522次,点赞7次,收藏8次。实现exit和wait(笔记,因为实现问题没有测试)https://blog.csdn.net/charlie114514191/article/details/146144946
相关文章:
从0开始的操作系统手搓教程45——实现exec
目录 建立抽象 实现加载 实现sys_execv !!!提示:因为实现问题没有测试。所以更像是笔记! exec 函数的作用是用新的可执行文件替换当前进程的程序体。具体来说,exec 会将当前正在运行的用户进程的进程体&…...

Android TCP封装工具类
TCP通信的封装,我们可以从以下几个方面进行改进: 线程池优化:使用更高效的线程池配置,避免频繁创建和销毁线程。 连接重试机制:在网络不稳定时,自动重试连接。 心跳机制:保持长连接ÿ…...

解决火绒启动时,报安全服务异常,无法保障计算机安全
1.找到控制面板-安全和维护-更改用户账户控制设置 重启启动电脑解决。...

Spring Boot框架总结(超级详细)
前言 本篇文章包含Springboot配置文件解释、热部署、自动装配原理源码级剖析、内嵌tomcat源码级剖析、缓存深入、多环境部署等等,如果能耐心看完,想必会有不少收获。 一、Spring Boot基础应用 Spring Boot特征 概念: 约定优于配置&#…...

为什么要使用前缀索引,以及建立前缀索引:sql示例
背景: 你想啊,数据库里有些字段,它老长了,就像那种 varchar(255) 的字段,这玩意儿要是整个字段都拿来建索引,那可太占地方了。打个比方,这就好比你要在一个超级大的笔记本上记东西,每…...

Nuxt3 ssr build/dev时区分不同的环境
package.json "scripts": {"build": "nuxt build --dotenv .env.prod","build:dev": "nuxt build --dotenv .env.dev","postbuild": "mv -f .output ./dist/.output", //支持自定义文件名"dev&quo…...

嵌入式学习第二十四天--网络 服务器
服务器模型 tcp服务器: socket bind listen accept recv/send close 1.支持多客户端访问 //单循环服务器 socket bind listen while(1) { accept while(1) { recv/send } } close 2.支持多客户端同时访问 (并发能力) 并发服务器 socket bind …...

tcp/ip协议配置参数有哪些?tcp/ip协议需要设置的参数有哪些
TCP/IP协议的配置参数是确保网络设备能够正确接入互联网并与其他设备进行通信的关键设置。这些参数主要包括以下几个方面: 1. IP地址 定义:IP地址是网络中设备的唯一标识符,用于标识和定位设备。它由32位二进制数组成,通常采用点…...

我有点担心开始AI中台了
有个特点历史教训是很难吸取的 从大数据开始就是一窝蜂的去搞,不管有没有什么数据量。反正要来个Hadoop。其实有些企业数据一块硬盘都放得下。 微服务来了,也不管自己的系统是不是适合微服务。我个人经验得出,to B和to G的业务场景…...

《用Python+PyGame开发双人生存游戏!源码解析+完整开发思路分享》
导语 "你是否想过用Python开发一款可玩性高的双人合作游戏?本文将分享如何从零开始实现一款类《吸血鬼幸存者》的生存射击游戏!包含完整源码解析、角色系统设计、敌人AI逻辑等核心技术点,文末提供完整代码包下载!" 哈…...

优选算法系列(1. 双指针_上)
目录 双指针 一:移动零(easy) 题目链接:移动零 解法: 代码: 二:复写零(easy) 题目链接:复写零 编辑 解法: 代码: 三:快乐…...

永洪科技深度分析实战,零售企业的销量预测
随着人工智能技术的不断发展,智能预测已经成为各个领域的重要应用之一。现在,智能预测技术已经广泛应用于金融、零售、医疗、能源等领域,为企业和个人提供决策支持。 智能预测技术通过分析大量的数据,利用机器学习和深度学习算法…...

c语言笔记 函数参数的等价(上)
这三种写法在 C 语言中是等价的,因为它们都用于声明一个指向二维数组的指针,或者用于声明一个二维数组作为函数参数。它们的等价性源于 C 语言中数组和指针之间的密切关系。让我们逐一分析这三种写法: 在C语言中,当数组作为函数参…...

hive面试题--left join的坑
student 表: 课程表course: 1、key为null, 不关联 select * from student s left join course c on s.id c.s_id;2、on中过滤条件 与 where 过滤条件区别 on and c.id<>‘1001’ 先过滤右表数据,然后与左表关联 select * from student s le…...

CEH与OSCP:网络安全认证对比分析
在网络安全领域,渗透测试被视为至关重要的一环,帮助企业检测和修复系统漏洞。为提升行业标准,许多认证应运而生,其中CEH和OSCP作为行业认可度较高的认证,广泛被网络安全从业者选择。尽管这两者都涉及渗透测试领域&…...

HTML 属性详解:为网页元素赋予更多功能
在构建网页的过程中,HTML 是基础的标记语言,而 HTML 属性则是为 HTML 元素提供附加信息的重要组成部分。 一、属性的基本概念与使用 属性通常出现在 HTML 标签的开始标签内,以 “name"value"” 的形式存在。这里的 “name” 是属…...

Ceph(2):Ceph简介
1 Ceph简介 Ceph使用C语言开发,遵循LGPL协议开源。Sage Weil(Ceph论文发表者)于2011年创立了以Inktank公司主导Ceph的开发和社区维护。2014年Redhat收购inktank公司,并发布Inktank Ceph企业版(ICE)软件,业务场景聚焦云…...

国产编辑器EverEdit - 设置文件类型关联为EverEdit
1 设置-文件关联 1.1 应用场景 文件关联是指在文件管理器中双击某类型的文件,操作系统自动调用可以打开该文件的应用程序,比如:用户双击XXXX.txt文件,系统默认会使用记事本打开该文件。 由于各行各业都会定义特有的文件类型&…...

2025网络安全工程师:软考新挑战与职业发展探析
网络安全工程师的崛起 随着信息技术的迅猛发展,网络安全问题日益凸显,网络安全工程师这一职业逐渐受到社会各界的广泛关注。特别是在2025年,随着各项网络安全法规的完善和实施,网络安全工程师的角色愈发重要。他们不仅是企业信息…...

设计模式之建造者模式:原理、实现与应用
引言 建造者模式(Builder Pattern)是一种创建型设计模式,它通过将复杂对象的构建过程分解为多个简单的步骤,使得对象的创建更加灵活和可维护。建造者模式特别适用于构建具有多个组成部分的复杂对象。本文将深入探讨建造者模式的原…...

【Leetcode 每日一题 - 补卡】2070. 每一个查询的最大美丽值
问题背景 给你一个二维整数数组 i t e m s items items,其中 i t e m s [ i ] [ p r i c e i , b e a u t y i ] items[i] [price_i, beauty_i] items[i][pricei,beautyi] 分别表示每一个物品的 价格 和 美丽值 。 同时给你一个下标从 0 0 0 开始的整数数…...

雪藏HsFreezer(游戏冻结工具) v2.21
HsFreezer 是一款让你可以随心冻结游戏的软件(游戏暂停软件、系统优化软件、进程管理软件),想玩就玩,想停就停,快捷键随心瞬发,单锁模式极致的丝滑切换,当然,不止适用游戏。更有丰富的特色系统优化功能。 PC主机,win掌机,笔记本--无脑装就对了,超大按键超大列表,触控盲操,非常巴…...

2019年蓝桥杯第十届CC++大学B组真题及代码
目录 1A:组队(填空5分_手算) 2B:年号字符(填空5分_进制) 3C:数列求值(填空10分_枚举) 4D:数的分解(填空10分) 5E:迷宫…...

前端安全面试题汇总及参考答案
目录 简述 XSS 攻击的原理及三种常见类型(存储型、反射型、DOM 型) 如何在前端防御 XSS 攻击?列举编码、过滤、CSP 策略的具体实现方式 富文本编辑器场景下如何安全处理用户输入的 HTML 内容? 如何通过 HttpOnly 属性增强 Cookie 安全性?它与 XSS 防御的关系是什么? …...

修复ubuntu下找不到音频设备的问题
出现问题的状态: ALSA 已正确识别到 ZOOM H2n 设备(card 1)sounddevice 库(依赖 PortAudio)未能正确枚举设备 修复方法: 1. 强制 sounddevice 使用 ALSA 后端 默认情况下,sounddevice 可能尝…...

⭐LeetCode周赛 3468. 可行数组的数目——暴力与数学⭐
⭐LeetCode周赛 3468. 可行数组的数目——暴力与数学⭐ 示例 1: 输入:original [1,2,3,4], bounds [[1,2],[2,3],[3,4],[4,5]] 输出:2 解释: 可能的数组为: [1, 2, 3, 4] [2, 3, 4, 5] 示例 2: 输入&…...

在线json转ArkTs-harmonyos
轻松将 JSON 数据转换为类型安全的 ArkTs 接口。快速准确地生成代码,提升开发效率,告别手动编写,让您的开发流程更加流畅! gotool...

Vue 实现AI对话和AI绘图(AIGC)人工智能
我司是主要是负责AIGC人工智能化平台的项目,俗称内容创作及智能工具平台。 授人以鱼不如授人以渔 首先我们要明白AIGC中前端需要做什么 会用到哪些技术栈 。 AIGC前端需要用到的技术栈:Vue,Markdown,SSE。就这个三件套。 前沿:有人觉得AI对…...

Visual Studio Code 基本使用指南
Visual Studio Code(简称 VSCode)是一款由微软开发的免费、开源、跨平台的代码编辑器,凭借其轻量级设计、丰富的插件生态和强大的功能,成为全球开发者的首选工具。本文将从安装配置到核心功能,全面解析 VSCode 的基本使…...

水下机器人推进器PID参数整定与MATLAB仿真
水下机器人推进器PID参数整定与MATLAB仿真 1. PID控制原理 目标:通过调节比例(P)、积分(I)、微分(D)参数,使推进器输出力快速稳定跟踪期望值。传递函数(示例):推进器动力学模型可简化为: [ G(s) = \frac{K}{\tau s + 1} \cdot e^{-Ts} ] 其中:K为增益,τ为时间常…...