【Linux取经路】初探进程地址空间
文章目录
- 一、历史问题回顾
- 二、语言层面的地址空间
- 2.1 验证
- 三、虚拟地址的引入
- 3.1 初步解释这种现象——引入地址空间的概念
- 3.2 再来粗粒度理解上面的现象
- 四、细节解释
- 4.1 地址空间究竟是什么?
- 4.2为什么要有地址空间
- 4.3 页表
- 4.3.1 CR3寄存器
- 4.3.2 页表是由页表项组成的
- 4.3.3 缺页中断
- 五、结语
一、历史问题回顾
之前在介绍 fork 函数的时候说过该函数返回了两次,至于为什么会返回两次,以及 fork 函数是如何做到返回两次的,在【Linux取经路】揭秘进程的父与子一文中已经做了详细的解释,忘了小伙伴可以点回去看看。在解释一个变量怎么会有两个不同值的时候,当时的说法是由于进程具有独立性,所以子进程把把父进程的数据拷贝了一份(写时拷贝),本质上是因为有两块空间,但是为什么同一个变量名可以让父子进程看到不同的内容当时并没有过多解释,因为这涉及到今天要讲解的程序地址空间问题,接下来就让我们来一探究竟吧。
二、语言层面的地址空间
相信大家在学习 C/C++ 语言的时候,一定都见过下面这张图:
小Tips:上图是以32位机为基础的,32位地址线最多可以表示 2 32 2^{32} 232个地址,因为每个地址线上只有0和1两种可能,最终也就是有4G个地址,一个地址对应一个字节,所以就是 2 32 2^{32} 232个字节,所以总的地址空间大小就是4G。内核空间是给操作系统使用的,一个进程对应的所有的数据结构对象都存储在内核空间。
2.1 验证
#include <stdio.h>
#include <stdlib.h> int g_val_1; // 定义一个未初始化全局变量
int g_val_2 = 100; // 定义一个已初始化全局变量
int main(int argc, char* argv[], char* env[])
{ printf("code addr:%p\n", main);// 函数名代表的就是地址,通过打印 main 函数的地址来查看代码区的地址 const char *str = "Hello word";// 定义一个字符串常量 printf("read only string addr:%p\n", str);// 字符常量区的地址 printf("init global value addr:%p\n", &g_val_2);// 已初始化全局变量的地址 printf("uninit global value addr:%p\n", &g_val_1); // 未初始化全局变量的地址 char* men1 = (char*)malloc(100); printf("heap addr-men1:%p\n", men1);// 堆区的地址 printf("stack addr-str:%p\n", &str); // 栈区的地址 static int a = 10;// 定义一个静态局部变量 printf("static a add:%p\n", &a); // 静态局部变量的地址 int i = 0; for(; argv[i]; i++) printf("argv[%d],addr:%p\n", i, argv[i]);// 打印命令行参数的地址 for(i = 0; env[i]; i++) printf("env[%d],addr:%p\n", i, env[i]);// 打印环境变量的地址 return 0;
}
小Tips:堆栈空间是相对“生长”的。即堆区是先使用低地址再使用高地址,而栈区是先使用高地址再使用低地址。
// 栈区地址由高向低增长
int main()
{ int a; int b; int c; int d; printf("stack addr:%p\n", &a); printf("stack addr:%p\n", &b); printf("stack addr:%p\n", &c); printf("stack addr:%p\n", &d);
}
// 堆区地址由低向高增长
int main()
{ char* mem1 = (char*)malloc(100); char* mem2 = (char*)malloc(100); char* mem3 = (char*)malloc(100); char* mem4 = (char*)malloc(100); printf("Heap addr:%p\n", mem1); printf("Heap addr:%p\n", mem2); printf("Heap addr:%p\n", mem3); printf("Heap addr:%p\n", mem4); return 0;
}
小Tips:这里还有一个小细节,堆栈地址其实相聚很远,原因是堆栈之间还有一块区域,这块区域在讲解动静态库的时候为大家讲解。
// 静态变量地址的验证
int g_val_1; // 定义一个未初始化全局变量
int g_val_2 = 100; // 定义一个已初始化全局变量
int main()
{ printf("code addr:%p\n", main);// 函数名代表的就是地址,通过打印 main 函数的地址来查看代码区的地址 const char *str = "Hello word";// 定义一个字符串常量 printf("read only string addr:%p\n", str);// 字符常量区的地址 printf("init global value addr:%p\n", &g_val_2);// 已初始化全局变量的地址 printf("uninit global value addr:%p\n", &g_val_1); // 未初始化全局变量的地址 char* men1 = (char*)malloc(100); printf("heap addr-men1:%p\n", men1); printf("stack addr-str:%p\n", &str); static int a = 10;// 定义一个静态局部变量 printf("static a add:%p\n", &a); return 0;
}
小Tips:从打印结果中可以看出,静态变量的地址和全局变量的地址十分接近。这是因为 static
修饰的局部变量,编译的时候已经被编译到全局数据区了。注意静态局部变量被编译到全局数据区仅仅是延长了该变量的生命周期,作用域并没有改变,还是只能在 main
函数的作用域中使用该变量。
注意:本小节的所有代码验证都是基于 Linux 操作系统的,相同的代码放在 Windows 操作系统中的 VS 下跑出来的结果可能会有所不同。
三、虚拟地址的引入
下面通过一个例子来引入虚拟地址的概念。
int g_val = 100;
int main()
{pid_t pid = fork();if(pid == 0){int cnt = 5;// 子进程while(1){printf("I am child, pid:%d, ppid:%d, g_val:%d, &g_val:%p\n", getpid(), getppid(), g_val, &g_val);sleep(1);if(cnt){cnt--;} else { g_val = 200; } } } else { // 父进程 while(1) { printf("I am parent, pid:%d, ppid:%d, g_val:%d, &g_val:%p\n", getpid(), getppid(), g_val, &g_val); sleep(1); } } return 0;
}
代码解释:上面这段代码中创建了一个子进程,并且定义了一个全局的变量 g_val
,初始化为100。让父子进程同时去访问变量 g_val
。在子进程要执行的代码片段中还定义了一个局部变量 cnt
,初始化为5,每执行一次循环就让 cnt--
,当 cnt
减到0的时候把 g_val
的值修改为200。
结果分析:从打印结果中可以看出,在子进程对 g_val
进行修改后,父子进程获取到的 g_val
的值并不一样,这符合我们的预期。因为父子进程相互独立,他们拥有各自的代码和数据,子进程在对 g_val
进行修改的时候会发生写时拷贝,这一点在前面的文章中已经讲过。但奇怪的是,为什么同一个地址,从该地址获取到的数据却不相同。那么真想就只有一个,这个打印出来的地址一定不是真实存在的物理地址,因为真实存在的物理地址中只能存放一个数据,不可能同时存储两个不同的数据。因此我们可以得出一个结论:我们代码中打印出来的地址不是物理地址,一般把这个地址叫做线性地址或者虚拟地址。
3.1 初步解释这种现象——引入地址空间的概念
之前在介绍进程的时候说过,一个进程就等于 task_struct
+ 代码和数据。但实际上事情并没有这么简单。一个进程一旦被创建出来,操作系统为了让该进程能够更好的运行,除了会为该进程创建对应的 PCB 对象之外,还会为这个进程创建一个地址空间(准确的叫法是进程地址空间)。我们平时在编码过程中使用的地址就是这个地址空间中的地址。进程地址空间本质上是内核为该进程创建的一个结构体对象。进程的 PCB 中是有对应的指针,指向该地址空间。进程地址空间中的虚拟地址和真是的物理地址是通过页表建立联系的,因此每个进程也会有一张页表。
小Tips:进程 PCB、进程地址空间、页表、物理地址四者之间的关系如上图所示。进程相关的代码和数据一定是存储在物理内存上的。
3.2 再来粗粒度理解上面的现象
根据进程独立性可知,每个进程都要有自己独立的 PCB、进程地址空间、页表。子进程的这些东西,大部分都是以父进程为模板创建出来的。对于全局变量 g_val
,在物理内存上它始终只有一份,在父进程中 g_val
有自己的虚拟地址0X601054,创建子进程的时候,子进程根据父进程的地址空间创建出自己的地址空间,此时 g_val
对应的虚拟地址仍然是0X601054。子进程的页表最初也是根据父进程的页表去创建的,因此子进程中 g_val
变量的虚拟地址和父进程中的一样,还是0X601054,并且子进程页表中的虚拟地址和物理地址的映射关系还是继承自父进程。因此,在子进程和父进程中都能够访问到 g_val
这个变量,并且在子进程和父进程中打印出来的 g_val
的地址都是一样的。父子进程共享同一份代码也是根据这个原理来实现的。当子进程要修改 g_val
变量的时候,由于父子进程的数据是相互独立的,该独立性体现在,在子进程去修改 g_val
的值不能影响到父进程,即,在父进程中 g_val
本身的值是100,在子进程将 g_val
的值修改成200的时候,父进程中 g_val
的值仍然得保持100。所以子进程在修改的时候会发生写时拷贝,其本质就是操作系统发现子进程要去修改父子进程所共享的数据,操作系统会说:“子进程你等会儿,先别改”。然后操作系统会为子进程在物理内存中开辟一块空间来存储 g_val
的值,最后修改页表中的虚拟地址0X601054所对应的物理地址。这就是为什么打印出来的是通过一个地址,但是却有两个不同的值。
小Tips:写时拷贝是操作系统自动完成的,子进程并不知情。这就相当于你有一个朋友要到家里来玩,但是家里有点乱,你让他等会再来,期间你收拾房子的这个过程你朋友并不知情。操作系统就相当于是你,子进程就相当于是你的朋友。重新开辟空间,但是在这个过程中,左侧的虚拟地址是0感知的,它不关心也不会影响它。
四、细节解释
4.1 地址空间究竟是什么?
所谓的进程地址空间,本质是一个描述进程可视范围的大小。并且地址空间内一定存在各种区域划分。因为每个进程将来都会有一个地址空间,所以操作系统一定会想办法把这些地址空间管理起来。管理的本质就是先描述再组织。在语言层面要描述一个事物只能通过结构体,因此地址空间本质是内核的一个数据结构对象,类似 PCB 一样。Linux 中描述地址空间的结构体是 struct mm_struct
,该结构体中通过定义 start
和 end
字段来确定地址空间的范围,以及进行区域划分。
struct mm_struct
{unsigned long start_code; // 代码段的开始地址unsigned long end_code; // 代码段的结束地址unsigned long start_data; // 数据的首地址unsigned long end_data; // 数据的尾地址unsigned long start_brk; // 堆的首地址unsigned long brk; // 堆的尾地址unsigned long start_stack; // 进程栈的首地址//...
};
小Tips:除了这些 start
和 end
对应的地址外,我们更应该注意在 start
和 end
范围内,连续的空间中,每一个最小单位都可以有地址,这个地址可以被直接使用。
4.2为什么要有地址空间
-
原因一:其中一个重要的原因是让进程以统一的视角看待内存。假如没有地址空间和页表,进程直接访问存储在内存上的数据和代码是非常麻烦的,首先进程需要将对应数据和代码的物理地址保存在进程 PCB 中,其次当一个进程从挂起状态或者阻塞状态被变成运行状态的时候,其数据和代码会被重新加载到内存中,此时物理地址大概率是会发生变化的,如果没有地址空间,就需要去修改进程 PCB 中的内容,这个过程是十分复杂的,并且我们的可执行程序加载到内存中并不一定是连续的,这一点会在后面进行讲解,因为可执行程序并不是一次性全部加载到内存中,这就导致我们的代码在物理内存中是东一块西一块,很乱。而地址空间的存在会帮我们解决这些问题,我们可以对可执行程序的虚拟地址进行连续的编址,这样就可以可以根据虚拟地址的先后顺序去“从头”去执行可执行程序,我们的进程不再需要担心可执行程序实际存储在内存的什么位置。这一点也可以总结成,地址空间的存在将物理内存中的代码和数据从无序变成了有序。
-
原因二:地址空间存在的第二个重要原因是,进程在访问内存的时候,增加一个转换的过程,在这个转换的过程中,可以对我们的寻址请求进行审查,一旦发生异常访问,比如越界访问,去修改只读数据等,会直接拦截,该请求不会到达物理内存,起到保护物理内存的作用。
-
原因三:因为有地址空间和页表的存在,将进程管理模块和内存管理模块进行解耦合。
小Tips:地址空间就相当于是操作系统给进程画的一个大饼。操作系统给每个进程划分的地空间范围都是0~4G(以32位操作系统为例)。每个进程都不知道对方的存在,都以为自己会独享这4G的空间。但实际并不是这样的,操作系统并不会一次性把这4G的空间给同一个进程,而是每个进程需要多少了就来申请多少。
4.3 页表
关于页表我们今天只做简单的介绍,真正的页表并不是向上图中画的那样只有两个字段,而是有多个字段并且分为多级页表。今天关于页表只介绍一下几点:
4.3.1 CR3寄存器
CPU 中有一个叫做 CR3 的寄存器(X86架构),该寄存器中保存的就是当前正在运行的进程的页表的地址,本质上属于当前进程的硬件上下文,当前进程被切换走了,是会把和该进程有关的所以硬件上下文数据带走的,当然就包括 CR3 寄存器中保存的该进程页表的首地址。该进程重新被 CPU 调度的时候,会把该这些硬件上下文数据重新加载到 CPU 里对应的寄存器中。CR3 寄存器中存的是物理地址。
4.3.2 页表是由页表项组成的
页表是有多个页表项组成的,一般的页表项有如下几个。Present(存在位):表示该页是否存在于物理内存当中。如果存在数据才可以访问。如果不存在,可能会引发缺页异常。Read/Write(读/写位):表示是否允许读取和写入。如果设置了“只读”,则只允许读取,写入会引发保护异常。代码段和字符常量区的数据所对应的页就是“只读”。User/Supervisor(用户/超级用户位):用于指示是否允许用户态程序访问该页。如果设置了“用户态可访问”,则用户程序可以访问;否则,只有内核态可以访问。这些权限位通过硬件(如CPU)来执行,当程序尝试访问内存时,硬件会检查相应的页表项权限位,如果权限不符合要求,会触发相应的异常,例如页故障异常。这样可以确保对内存的合法访问,提高系统的安全性和稳定性。
小Tips:这里需要注意一下,单纯的物理内存是没有“只读”、“只写”等这些权限管理概念的,对于物理内存上的任何一块空间来说,都应该是可读可写的。所以我们在语言层面所说的代码区和字符常量区是只读的,本质上是因为存储代码和字符常量的物理内存所对应的页中设置了“只读”权限。
#include <stdio.h> int main()
{ char* str = "Hello World"; *str = 'B'; return 0;
}
小Tips:上面这段代码一看就是错误的写法。因为正常来说是需要在 char* str
的前面加上 const
。但是加上后,这段代码会在编译时就报错(语法上说不过去,被 const
修饰后是不能再进行修改的)。而不加则是在运行时报错,这就是这段代码在运行时通过页表去 *str
中的内容,被硬件检查出不符合要求,进而产生了异常。
4.3.3 缺页中断
现代操作系统几乎不做任何浪费空间和时间的事情,操作系统对大文件可以实现分批加载。这个其实很好理解,我们平时玩的大型电脑游戏动辄就几十个G,而我们的内存一般就只有8个G或者16个G,所以我们在玩这种大型游戏的时候,一定没有把和该游戏有关的所有文件一次性加载到内存中,而是采用分批加载的策略。操作系统对可执行程序一般采用的是惰性加载机制,即操作系统承诺给进程分配4G的空间(虚拟内存的大小),但实际在物理内存上是用多少加载多少。通过页表中的Present(存在位)页表项去判断,去标记该页是否存在于物理内存中,如果不存在就会发生缺页中断,将需要的代码和数据加载到物理内存中。可不要小瞧加载过程,这里会涉及到物理内存这么大,该申请那块儿内存,加载是加载可执行程序的那一部分,加载完后物理地址如何填到页表里呢?这一系列和缺页中断相关的问题最终都是由操作系统中的内存管理模块来执行的。整个缺页中断的过程对进程是不可见的。正是因为地址空间和页表的存在,才实现了进程管理和内存管理在软件层面的解耦。进程要访问对应的代码和数据只需要知道虚拟地址即可,内存管理模块根据虚拟地址去判断是否要进行缺页中断。
小Tips:进程在被创建的时候,一定是先创建内核数据结构(进程 PCB、地址空间、页表…),然后再加载对应的可执行程序。挂起状态就是将进程的代码和数据从内存中清出去,然后再将Present(存在位)标志位设置成不存在即可。
五、结语
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!
相关文章:

【Linux取经路】初探进程地址空间
文章目录 一、历史问题回顾二、语言层面的地址空间2.1 验证 三、虚拟地址的引入3.1 初步解释这种现象——引入地址空间的概念3.2 再来粗粒度理解上面的现象 四、细节解释4.1 地址空间究竟是什么?4.2为什么要有地址空间4.3 页表4.3.1 CR3寄存器4.3.2 页表是由页表项组…...

.net core 6 使用注解自动注入实例,无需构造注入 autowrite4net
像java使用autowrite一样使用 1、前提先注册到ioc容器当中 builder.Services.AddScoped 2、nuget引入AutoWrite4Net 3、启用 //启用自动注入 app.UseAutoWrite(); 4、在类上使用注解 [StartAutoWrite] public class NacosController : ControllerBase 5、实例上使用注解 …...

LeetCode、2300. 咒语和药水的成功对数【中等,排序+二分】
文章目录 前言LeetCode、2300. 咒语和药水的成功对数【中等,排序二分】题目及类型思路及代码 资料获取 前言 博主介绍:✌目前全网粉丝2W,csdn博客专家、Java领域优质创作者,博客之星、阿里云平台优质作者、专注于Java后端技术领域…...

【MyBatis-Plus】逻辑删除
对于一些比较重要的数据,我们通常采用逻辑删除。(即用一个字段表示是否删除,实际上始终在数据库没有被删除) 当逻辑删除字段为 true,业务处理的时候会自动把该数据当做一个“不存在”的数据处理。(即不处理…...

Rust基础语法1
所有权转移,Rust中没有垃圾收集器,使用所有权规则确保内存安全,所有权规则如下: 1、每个值在Rust中都有一个被称为其所有者(owner)的变量,值在任何时候只能有一个所有者。 2、当所有者离开作用域…...

【算法基础 数学】快速幂
题目描述 给定 n n n组 a i , b i , p i a_i,b_i,p_i ai,bi,pi,对于每组数据,求出 a i b i m o d p i a_i^{b^i}~mod~p_i aibi mod pi 的值。 样例 输入样例: 2 3 2 5 4 3 9输出样例: 4 1快速幂解决的问题 用来…...

2024年华为OD机考高分攻略-完整题库-两周350分
华为OD是个不错的机会,很适合非软件行业到软件行业的转身。 但是很多同学之前没有软件基础,不知道该如何高效的准备OD机考。 我是一名软件培训老师,我的学生有上百人顺利通过了华为OD机考,并取得了高分,我将经验分享…...

【微信小程序独立开发 4】基本信息编辑
这一节完成基本信息的编辑和保存 首先完成用户头像的获取 头像选择 需要将 button 组件 open-type 的值设置为 chooseAvatar,当用户选择需要使用的头像之后,可以通过 bindchooseavatar 事件回调获取到头像信息的临时路径。 从基础库2.24.4版本起&…...

Docker-基础指令
前置知识 docker官网地址:https://www.docker.com/ docker镜像地址:https://hub.docker.com/ docker安装教程:https://docs.docker.com/engine/install/centos/ 安装只需要注意将仓库源改为国内就好,推荐去阿里云注册自己的账号获得加速地址…...

JUC-Java内存模型JMM
JMM概述 Java Meory Model java内存模型。在不同的硬件和不同的操作系统上,对内存的访问方式是不一样的。这就造成了同一套java代码运行在不同的操作系统上会出问题。JMM就屏蔽掉硬件和操作系统的差异,增加java代码的可移植性。这是一方面。 另一方面JM…...

uni-app使用HBuilderX打包Web项目
非常简单,就是容易忘记 一、找到manifest.json配置Web配置 二、源码视图配置 "h5" : {"template" : "","domain" : "xxx.xx.xx.xxx","publicPath" : "./","devServer" : {&quo…...

前后置、断言、提取变量、数据库操作功能
前置操作和后置操作都是 API 请求在发送和响应过程中执行的脚本,主要用于在发起 API 请求前和获得响应后完成验证或执行某些操作,目的是为了提高 API 调试和测试的效率,并确保接口的正确性。 前置操作 前置操作是在 API 请求之前执行的脚本…...

三子棋/井字棋(C语言)
这个游戏需要用到三个文件 game.h头文件用来申明函数和导包 game.h如下: #pragma once #define ROW 3 #define COL 3 #include <stdlib.h> #include <time.h> #include <stdio.h>//初始化棋盘的函数void InitBoard(char board[ROW][COL], int row, int co…...

数据结构小项目----通讯录的实现(这里用链表实现) 超详细~~~~૮(˶ᵔ ᵕ ᵔ˶)ა
目录 Contact.h说明: 结构体与头文件的包含: 编辑 函数在头文件的声明与定义: Contact.c中各个函数的实现: 1.检查链表中的数据是否满了,满了就扩容 2.链表的尾插 3.链表的删除 4.查找名字是否匹配 5.初始化通讯…...

Electron Apple SignIn 登录
本人写博客,向来主张:代码要完整,代码可运行,文中不留下任何疑惑。 最讨厌写博客,代码只留下片段,文中关键的东西没写清楚。之前看了那么多文章,就是不告诉我clientId从哪来的。 官方资料地址&…...

常用中间件漏洞
IIS6 IIS7 安装 控制面板-----打开关闭windows功能 添加角色-----添加IIS 启动之后访问localhost 复现 服务器换成IIS7 访问报错 大概就是缺少CGI模块 问题解决 添加php-cgi的路径 添加脚本映射 修改php.ini文件 将 cgi.fix_pathinfo1 然后设置一个图片 访问 在后缀加上/.…...

Windows系统使用手册
点击前往查看🔗我的博客文章目录 Windows系统使用手册 文章目录 Windows系统使用手册Windows10解决大小核调度问题Windows系统安装软件Windows系统Typora快捷键Windows系统压缩包方式安装redisWindows安装dockerWindows系统的docker设置阿里源Windows系统下使用doc…...

mp4文件可以转成mp3音频吗
现在是个非常流行刷短视频一个年代,刷短视似乎成了人们休闲娱乐的一种方式,在日常刷短视频过程中,肯定会有很多同学被短视频 bgm 神曲洗脑,比如很多被网红翻唱带火的歌曲,例如其中"不负人间”,就是其中…...

Java-IO流【登录注册小项目】
♥️作者:白日参商 🤵♂️个人主页:白日参商主页 ♥️坚持分析平时学习到的项目以及学习到的软件开发知识,和大家一起努力呀!!! 🎈🎈加油! 加油!…...

数字化金融时代:探讨全球金融科技创新的最新动态
在当今数字化金融时代,金融科技创新如影随形,迅猛发展。本文将深入探讨全球范围内金融科技的最新动态,剖析各地新兴趋势与突破。从区块链技术的应用到人工智能在金融领域的崭露头角,我们将一一解读这个正在不断演变的金融科技画卷…...

LeetCode:206. 反转链表
力扣链接 算法思想:由于单链表是单向的,想要对当前元素进行操作,需找到前一个元素。本题利用双指针,初始pre指针指向NULL,cur指针指向head.再对局部翻转之前,先把下一个结点存到temp指针中。当进行完如下代…...

linux 安装nginx
介绍 官网 https://nginx.org/en/download.html 在安装nginx前首先要确认系统中安装了gcc、pcre-devel、zlib-devel、openssl-devel linux 检查是否安装过某软件包 yum -y install gcc pcre-devel zlib-devel openssl openssl-devel #下载 wget https://nginx.org/downloa…...

1.C语言——基础知识
C语言基础知识 1.第一个C语言程序2.注释3.标识符4.关键字5.数据类型6.变量7.常量8.运算符9.输入输出输入输出 1.第一个C语言程序 C语言的编程框架 #include <stdio.h> int main() {/* 我的第一个 C 程序 */printf("Hello, World! \n");return 0; }2.注释 单行…...

Redis 存在线程安全问题吗?为什么?
一个工作了 5 年的粉丝私信我。 他说自己准备了半年时间,想如蚂蚁金服,结果第一面就挂了,非常难过。 问题是: “Redis 存在线程安全问题吗?” 一、问题解析 关于这个问题,我从两个方面来回答。 第一个&a…...

无人机测绘助力实现高效、安全的城市规划
随着城市化进程的不断加快,城市规划显得尤为重要。而无人机测绘技术作为一种创新的工具,为城市规划提供了更加高效、安全的解决方案。它通过快速、精确的数据采集和分析,为行业提供有力的决策支持,助力城市规划的现代化和可持续发…...

实验七 RMAN恢复管理器
🕺作者: 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux 😘欢迎关注:👍点赞🙌收藏✍️留言 🏇码字不易,你的👍点赞🙌收藏❤️关注对我真的很重要&…...

未来 AI 可能给哪些产业带来哪些进步与帮助?
AI时代如何要让公司在创新领域领先吗?拥抱这5种创新技能,可以帮助你的公司应对不断变化。包括人工智能、云平台应用、数据分析、 网络安全和体验设计。这些技能可以帮助你提高业务效率、保护公司知识资产、明智决策、满足客户需求并提高销售额。 现在就加…...

Java医院信息管理系统
技术框架: springboot shiro layui jquery thymeleaf nginx 有需要的可以联系我。 运行环境: jdk8 mysql IntelliJ IDEA maven项目功能: 本项目是用springbootlayuishiro写的医院管理系统,系统的业务比较复杂&#x…...

QT+OSG/osgEarth编译之八十:ive+Qt编译(一套代码、一套框架,跨平台编译,版本:OSG-3.6.5插件库osgdb_ive)
文章目录 1、osgdb_ive介绍2、文件分析3、pro文件4、编译实践1、osgdb_ive介绍 通过osgdb_ive,OpenSceneGraph开源库能方便地读取ive格式的三维文件。 ive(Interchangeable Virtual Environment)是一种三维图形数据交换格式,主要用于虚拟现实和增强现实领域的场景数据交…...

Webpack5入门到原理3:基本配置
在开始使用 Webpack 之前,我们需要对 Webpack 的配置有一定的认识。 5 大核心概念 entry(入口) 指示 Webpack 从哪个文件开始打包 output(输出) 指示 Webpack 打包完的文件输出到哪里去,如何命名等 l…...