从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 会将当前正在运行的用户进程的进程体&…...
深入理解 Linux 中的 -h 选项:让命令输出更“人性化”
在 Linux 系统中,命令行工具是系统管理员和普通用户最常用的交互方式之一。然而,命令行输出往往充满了技术性术语和数字,对于初学者或非技术用户来说可能显得晦涩难懂。幸运的是,许多 Linux 命令都提供了一个非常实用的选项&#…...
23. 观察者模式
原文地址: 观察者模式 更多内容请关注:智想天开 1. 观察者模式简介 观察者模式(Observer Pattern)是一种行为型设计模式,用于建立对象之间的一种一对多的依赖关系。当一个对象的状态发生变化时,所有依赖于它的对象都…...
sql语句分页的关键字是?
在 SQL 中,分页通常是通过限制查询结果的数量并指定从哪一行开始获取数据来实现的。不同的数据库系统使用不同的分页关键字。 以下是常见数据库系统的分页关键字: MySQL / PostgreSQL / SQLite 使用 LIMIT 和 OFFSET 来进行分页: LIMIT 限…...
golang从入门到做牛马:第十四篇-Go语言结构体:数据的“定制容器”
在Go语言中,结构体是一种非常强大的数据结构,它允许你将不同类型的数据组合在一起,形成一个逻辑上的“记录”。结构体非常适合用来表示复杂的数据类型,比如一个图书馆的书籍记录、一个用户的信息等。接下来,让我们一起深入了解Go语言中的结构体。 什么是结构体:数据的“组…...
C#控制台应用程序学习——3.11
一、整型数字计算 如果我们想执行以下程序:程序提示用户输入一个数字并输出 num 20 的结果,我们的思维应该是这样的: using System;public class Class1 {public static void Main(string[] args){Console.WriteLine("Enter the first…...
【商城实战(13)】购物车价格与数量的奥秘
【商城实战】专栏重磅来袭!这是一份专为开发者与电商从业者打造的超详细指南。从项目基础搭建,运用 uniapp、Element Plus、SpringBoot 搭建商城框架,到用户、商品、订单等核心模块开发,再到性能优化、安全加固、多端适配…...
STM32之硬件SPI
SPI1和SPI2挂载的总线不一样,SPI1的时钟频率的比SPI2的大一倍。 核心部分是移位寄存器,数据一位一位的移到MOSI,同理,移位寄存器也一位一位的从MISO接收数据,LSBFIRST控制位控制高位先行还是低位先行。移位寄存器左边交叉箭头是ST…...
【Go每日一练】构建一个简单的用户信息管理系统
👻创作者:丶重明 👻创作时间:2025年3月7日 👻擅长领域:运维 目录 1.😶🌫️题目:简单的用户信息管理系统2.😶🌫️代码开发3.😶&a…...
【力扣】2629. 复合函数——函数组合
【力扣】2629. 复合函数——函数组合 文章目录 【力扣】2629. 复合函数——函数组合题目解决方案概述方法 1:使用迭代的函数组合概述算法实现复杂度分析 方法 2:使用 Array.reduceRight() 的函数组合概述算法实现复杂度分析 附加考虑处理 this 上下文使用…...
【网络协议安全】任务10:三层交换机配置
CSDN 原创主页:不羁https://blog.csdn.net/2303_76492156?typeblog三层交换机是指在OSI(开放系统互连)模型中的第三层网络层提供路由功能的交换机。它不仅具备二层交换机的交换功能,还能实现路由功能,提供更为灵活的网…...
Linux 服务器安全配置:密码复杂度与登录超时设置
Linux服务器安全配置指南:密码复杂度与登录超时设置 一、密码复杂度设置 通过PAM模块pam_cracklib.so实现密码强度策略,配置文件: system-auth该文件主要用于定义系统范围内的认证策略,涵盖了用户登录、su 命令切换用户、sudo 权限提升等多种认证场景。当用户尝试进行系…...
依托大数据实验室建设,培育创新人才:数据科学与大数据技术专业人才培养实践
近年来,得益于全球大数据产业政策扶持与数字经济蓬勃发展,大数据市场呈现迅猛增长态势。国家层面相继出台《“数据要素”三年行动计划(2024—2026年)》《数字中国建设整体布局规划》等政策,旨在激发产业创新活力&#…...
如何使用 CSS 实现黑色遮罩效果
最近在工作中遇见了一个需求,鼠标经过盒子出现黑色遮罩,遮罩中有相关的编辑按钮,点击以后,进行图片上传并且展示,由于当时没有思路,思考了好久,所以在完成开发后进行总结,使用的技术…...
ChatGPT课件分享(37页PPT)
资料解读:ChatGPT课件分享 详细资料请看本解读文章的最后内容。 近年来,人工智能技术的迅猛发展引发了全球范围内的广泛关注,尤其是以OpenAI为代表的公司在自然语言处理领域的突破性进展,彻底改变了人机交互的方式。本文将详细解…...
开源模型时代的 AI 开发革命:Dify 技术深度解析
开源模型时代的AI开发革命:Dify技术深度解析 引言:AI开发的开源新纪元 在生成式AI技术突飞猛进的2025年,开源模型正成为推动行业创新的核心力量。据统计,全球超过80%的AI开发者正在使用开源模型构建应用,这一趋势不仅…...
无人机扩频技术对比!
一、技术原理与核心差异 FHSS(跳频扩频) 核心原理:通过伪随机序列控制载波频率在多个频点上快速跳变,收发双方需同步跳频序列。信号在某一时刻仅占用窄带频谱,但整体覆盖宽频带。 技术特点: 抗干扰…...
C语言_数据结构总结4:不带头结点的单链表
纯C语言代码,不涉及C 0. 结点结构 typedef int ElemType; typedef struct LNode { ElemType data; //数据域 struct LNode* next; //指针域 }LNode, * LinkList; 1. 初始化 不带头结点的初始化,即只需将头指针初始化为NULL即可 void Init…...
Zama TFHE-rs v1.0 发布
1. 引言 2025年2月,Zama 发布了 TFHE-rs v1.0,这是 TFHE-rs 库的第一个稳定版本。这标志着一个重要的里程碑,稳定了 x86 CPU 后端的高级 API,同时确保了向后兼容性。——即,现在可以依赖 TFHE-rs API,而不…...
AArch64架构及其编译器
—1.关于AArch64架构 AArch64是ARMv8-A架构的64位执行状态,支持高性能计算和大内存地址空间。它广泛应用于现代处理器,如苹果的A系列芯片、高通的Snapdragon系列,以及服务器和嵌入式设备。 • 编译器:可以使用GCC、Clang等编译器编…...
【ISP】对于ISP的关键算法补充
本篇是对于ISP的关键算法进行补充说明, 后面我们将开始逐渐深入讨论ISP的pipeline 1. 非局部均值(NLM, Non-Local Means) 原理 非局部均值(NLM)是一种基于 块匹配(Patch Matching) 的去噪算法…...
几种常见的虚拟环境工具(Virtualenv、Conda、System Interpreter、Pipenv、Poetry)的区别和特点总结
在 PyCharm 中创建虚拟环境是一个非常直接的过程,可以帮助你管理项目依赖,确保不同项目之间的依赖不会冲突。 通过 PyCharm 创建虚拟环境 打开 PyCharm 并选择或创建一个项目。 打开项目设置: 在 Windows/Linux 上,可以通过点击…...
Ubuntu安装问题汇总
参考文章: 【Ubuntu常用快捷键总结】 【王道Python常用软件安装指引】 1. 无法连接虚拟设备 sat0:0 【问题】:出现下图所示弹框。 【问题解决】: 点击 “否” 。 点击左上角的 “虚拟机” → “设置…” → “CD/DVD (SATA)” ,…...
Ceph(1):分布式存储技术简介
1 分布式存储技术简介 1.1 分布式存储系统的特性 (1)可扩展 分布式存储系统可以扩展到几百台甚至几千台的集群规模,而且随着集群规模的增长,系统整体性能表现为线性增长。分布式存储的水平扩展有以下几个特性: 节点…...
从0开始的操作系统手搓教程43——实现一个简单的shell
目录 添加 read 系统调用,获取键盘输入 :sys_read putchar和clear 上班:实现一个简单的shell 测试上电 我们下面来实现一个简单的shell 添加 read 系统调用,获取键盘输入 :sys_read /* Read count bytes from the file pointed to by fi…...
【Spring】基础/体系结构/核心模块
概述: Spring 是另一个主流的 Java Web 开发框架,该框架是一个轻量级的应用框架。 Spring 是分层的 Java SE/EE full-stack 轻量级开源框架,以 IoC(Inverse of Control,控制反转)和 AOP(Aspect…...
01 音视频知识学习(视频)
图像基础概念 ◼像素:像素是一个图片的基本单位,pix是英语单词picture的简写,加上英 语单词“元素element”,就得到了“pixel”,简称px,所以“像素”有“图像元素” 之意。 ◼ 分辨率:是指图像…...
vue3自定义hooks遇到的问题
问题 写了一个输入查询参数和url返回加载中状态、请求方法、接口返回列表的hooks,出现的结果是只有请求方法有效,加载状态无效,接口返回了数据,页面却不显示数据。 代码如下 只展示部分关键代码 import { ref, toRefs, toRef, o…...
用Python和Docker-py打造高效容器化应用管理利器
《Python OpenCV从菜鸟到高手》带你进入图像处理与计算机视觉的大门! 解锁Python编程的无限可能:《奇妙的Python》带你漫游代码世界 随着容器化技术的发展,Docker已成为现代化应用部署的核心工具。然而,手动管理容器在规模化场景下效率低下。本文深入探讨如何利用Python结…...
liunx磁盘挂载和jar启动命令
一、磁盘挂载 查看历史磁盘挂载命令:history | grep mount 查看所有挂载硬盘命令:mount 磁盘挂载命令:mount -t cifs -o usernamesh**,passwordP!ss**** //192.168.1.2/attachmentfilesShare2.2/pdfCert /home/nybzg/cnfai1/pdfCert 二、j…...
