【Linux进程】进程地址空间

news2025/1/24 22:48:20

目录

程序地址空间回顾

进程地址空间

宏观理解

谈细节

1、进程地址空间究竟是什么?

2、管理地址空间

3、页表

总结几个问题:

1、为什么要有进程地址空间?

2、进程切换

3、进程具有独立性,怎么做到的?


程序地址空间回顾

我们之前学习C语言的时候,应该见过下面的空间布局图

 现在我问大家一个问题,这个东西是内存吗?其实不是!我们这里先把它叫做地址空间。是什么地址空间后面再来说。

现在我们先来用代码验证一下上面这张图。

代码:

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 
  4 int g_val_1;
  5 int g_val_2 = 100;
  6 int main()
  7 {
  8     printf("code addr: %p\n", main);//代码区
  9     const char *str = "hello bit";
 10     printf("read only string addr: %p\n", str);//字符常量区,str存放的是字符串首元素的地址,也就是h的地址
 11     printf("init global value addr: %p\n", &g_val_2);//已初始化全局变量
 12     printf("uninit global value addr: %p\n", &g_val_1);//未初始化全局变量
 13     char *mem = (char*)malloc(100);
 14     char *mem1 = (char*)malloc(100);
 15     char *mem2 = (char*)malloc(100);
 16     printf("heap addr: %p\n", mem);//堆区,这里直接打印men就行,因为要的是堆区地址
 17     printf("heap addr: %p\n", mem1);
 18     printf("heap addr: %p\n", mem2);
 19     printf("stack addr: %p\n", &str);//str是在main中创建的,所以是str的地址是栈上创建的
 20     printf("stack addr: %p\n", &mem);//mem是在main中创建的,所以是mem的地址是栈上创建的
 21     int a = 0;
 22     int b;
 23     int c;
 24     printf("a = stack addr: %p\n", &a);
 25     printf("stack addr: %p\n", &b);
 26     printf("stack addr: %p\n", &c);
 27                                                                                                                                                                                        
 28     return 0;
 29 }
~

运行结果:

 我们在上面代码中把变量a改为static变量

运行结果:

我们发现static修饰的局部变量的地址是在全局数据区的,也就是说虽然static修饰的局部变量作用域是在函数内,但是他的声明周期已经是全局变量了。

我们再来看下面这一段代码:

 1: myproc.c  ⮀                                                                                                                                                               ⮂⮂ buffers 
  1 #include<stdio.h>  
  2 #include<stdlib.h>  
  3 #include<unistd.h>                                                                                                                                                                     
  4                                                 
  5 int g_val = 100;                                
  6                                                 
  7 int main()                                      
  8 {                                               
  9     pid_t id = fork();                          
 10     if(id == 0)                                 
 11     {                                           
 12         int cnt = 5;                            
 13         // 子进程                               
 14         while(1)                                                                                                     
 15         {                                                                                                            
 16             printf("i am child, pid : %d, ppid : %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);  
 17             sleep(1);                           
 18             if(cnt) cnt--;                      
 19             else {                                          
 20                 g_val=200;                                  
 21                 printf("子进程change g_val : 100->200\n");  
 22                 cnt--;                          
 23             }                                   
 24         }                                       
 25     }                                           
 26     else                                        
 27     {                                           
 28         // 父进程                               
 29         while(1)                                                                                                      
 30         {                                                                                                             
 31             printf("i am parent, pid : %d, ppid : %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);  
 32             sleep(1);                           
 33         }                                       
 34     } 

        return 0;                                          
 35 } 

运行结果:

看完结果我们会有疑问:怎么可能父子进程中g_val变量的地址是一样的,同时读取,打印出来的值却是不一样的。

如果变量的地址是物理地址,是不可能存在上面的现象的。

所以我们这里先给出以下结论:

  • 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
  • 但地址值是一样的,说明绝对不可能是物理地址
  • 在Linux地址下,这种地址叫做 虚拟地址
  • 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理。

注意:OS必须负责将 虚拟地址 转化成 物理地址

进程地址空间

宏观理解

所以之前说‘程序的地址空间’是不准确的,准确的应该说成 进程地址空间 ,那该如何理解呢?

我们来看下面这张图并从宏观层面上理解上面遗留的问题——为什么父子进程中g_val变量的地址是一样的,同时读取,打印出来的值却是不一样的?

  • 每个进程=内核数据结构(task_struct&&地址空间(mm_struct)&&页表)+程序的代码和数据
  • 页表是一个key value结构,地址空间的虚拟地址可以通过页表映射到物理内存当中的物理地址。 

当子进程要对g_val进行修改时,操作系统识别到子进程要将“0x60105c”进行写入,然后就通过页表映射到对应物理地址。但是操作系统发现,这段物理地址是和父进程共享的。因为进程是具有独立性的,修改数据的时候不能影响到对方,操作系统进行了写时拷贝(是由操作系统自动完成的),这里写时拷贝的本质其实就是在物理内存重新开辟了一段空间,然后将子进程的虚拟地址指向新的物理地址。然后再将g_val修改成200。

注意:在这个过程中,左侧的虚拟地址是0感知的,不会影响它。

谈细节

1、进程地址空间究竟是什么?

举个例子:

我们每中国人想要游玩自己的国家,我们都有960万平方公里的空间让我们去访问,即使我们没有去过所有地方。

下面我们再来举一个例子:

有个美国富人有10亿美金,但是他的私生活比较混乱,有3个私生子。

但是每个私生子并不知道他们的父亲还有其他的私生子, 所以每个私生子都认为自己在独占大富翁的财富。 

三个私生子都认为自己是独自继承老爹的10亿的,所以他们都认为自己有10亿,因此也就会出现下面这种情况:

私生子1今天说:我想买台车,老爹你给我200w吧,老爹说:没问题。

私生子2今天说:我想买块表,老爹你给我40w吧,老爹说:没问题。

私生子3今天说:我想娶个老婆,老爹你先给100w吧,老爹说:没问题。

因为他们三个都是这个富翁的私生子,富翁会去尽可能的满足他们的要求。

富翁对私生子1说:你要好好读博士,等爸爸老了之后财产就由你来继承。

富翁对私生子2说:你要好好的工作,多历练历练,积累一下工作经验,等爸爸老了之后财产就由你来继承。

富翁对私生子3说:你要好好的打篮球,成为一名职业的篮球运动员,等爸爸老了之后财产就由你来继承。

这个时候私生子1觉得自己继承了老爹的财产之后,自己就会拥有10亿,然后去投资等等行为。同样的私生子2和私生子3也会绝对自己继承了老爹的财产之后会有10亿,然后想着如何去处理这10亿。

但其实这都是富翁对私生子们画的大饼。这里的大饼就相当于进程地址空间。富翁相当于操作系统。进程就相当于这里的私生子。

总结:每个私生子都被画了一张大饼,都认为自己有10亿,但是富翁并不会把自己所以的钱给任何一个进程,也就是说每个进程都有一个地址空间,都认为自己在独占物理内存。

所以,所谓的进程地址空间,本质是一个描述进程可视范围的大小。

2、管理地址空间

地址空间本质是内核的一个数据结构对象,类似PCB一样,在Linux当中进程地址空间由结构体mm_struct实现。地址空间也是要被操作系统管理的: 先描述,再组织

那么问题又来了,如何通过mm_struct这个结构体去描述进程地址空间里面特定的区域的呢?

我们来举个例子:

小胖和小花是小学同桌,但是小胖不太讲卫生,天天鼻涕口水留个不停还往桌子上面蹭,他的同桌是一个小花,因为小胖不讲卫生,所以就特别嫌弃小胖。因此小花就拿尺子给桌子划了一条线:三八线!!!小花对小胖说:你要是越过这条线我就打你。

这里小花划线的本质其实就是——划分区域!!!

 

我们用c语言描述这个过程,就相当于定义一个结构体,里面存放了小胖和小花的各自的空间,我们只需要将他们的起始位置和结束位置记录下来即可。

所谓的空间区域调整变大,或者变小如何理解?

比如小胖越过38线了,小花将小胖的空间没收一部分,就只需要修改结构体中的小胖的结束位置,和小花的起始位置即可。

那我们如何判断小胖有没有越界呢?

只要当我们访问的地址不在小胖的起始位置和结束位置,就可以判断小胖越界了。

但在范围内,连续的空间中,每一个最小单位都可以有地址,这个地址可以被小胖直接使用!!!

类似的,我们mm_struct也可以通过这样的方式来给进程地址空间划分区域从而去描述它。

地址空间内一定要存在各种区域划分,也对线性地址进行start,和end即可

虽然这里只有start和end但是每个进程都可以认为mm_struct代表整个内存,且所有的地址为0x00000000~0xffffffff。因为虚拟地址是由0x00000000到0xffffffff线性增长的,因此虚拟地址又叫做线性地址。

每个进程都认为地址空间的划分是按照4GB空间划分的,换言之也就是说每个进程都认为自己拥有4GB。

现在我知道了mm_struct是如何描述进程地址空间的,那么我现在又有一个问题:

3、页表

前面我们讲到,页表是一个key value结构,地址空间的虚拟地址可以通过页表找到物理内存当中的物理地址。

首先进程必须是处于运行阶段才能访问到自己的内存,每个进程是由内核数据结构(task_struct&&地址空间(mm_struct)&&页表)+程序的代码和数据组成的。task_struct中存放了自己的地址空间的地址。系统为了能够实现虚拟地址到物理地址之间的映射,维护了页表结构

接下来我们补充几个关于页表的知识点

(1)进程在进行虚拟地址转换到物理地址的时候,会不会找不到页表呢?

答案是不会的,我们来看下面这个图

CPU中有一个cr3寄存器,里面存放了当前进程的页表起始地址而我们的寄存器存放的页表地址本质上属于进程的硬件上下文,当进程切换的时候会被带走当进程重新运行的时候页表地址就会被重新加载到CPU,所以每个进程自始至终都能找到自己的页表,不存在找不到页表地址的情况。

当我们要进行虚拟地址向物理地址映射的时候,CPU通过cr3寄存器中的页表地址找到页表,然后进行虚拟地址向物理地址映射。

(2)页表还有权限标志位,可以很好的进行权限管理

我们先来看一段代码:

    1 #include<stdio.h>
    2 #include<stdlib.h>
    3 #include<unistd.h>
    4 
    5 int main()
    6 {
    7     char *str = "hello process";
    8     *str = 'H';                                                                                                                                                                      
    9     return 0;
   10 }

运行结果:

可以编译,但是执行报错

我们知道这里str是字符串hello process的首地址,也就是字符h的地址,*str就是h,我们将h改为H明显是不行的,因为h是属于字符常量区,字符常量区是只读的,不可以修改。 

但上面的结论我们之前都是记住的,现在我们想知道为什么代码区和字符常量区是只读?怎么做到的呢? 

我们来看下面这个图

其实页表条目当中还有对应的标志位,来标识当前的物理内存是可读还是可写,假设我们虚拟地址为0x123456的变量是可读可写的,对应页表的读写标志位就是可读可写的。

比如现在代码区有虚拟地址0x1111映射到了物理内存的0x12位置,但是页表当中标记的这个位置是只读的。如果我们这个时候要往这个位置进行写入。CPU将虚拟地址0x1111通过页表转换到物理地址0x12,发现这个位置是标记成只读的。如果我们这个时候想向该位置进行写入,就相当于是非法操作,系统就会进行拦截,把我们这个进程给挂掉。所以,页表可以给我们很好的进行权限管理。

同样的,这时候我们就可以解决上面的问题,代码区和字符常量区他们在页表当中所匹配的虚拟地址和物理地址的映射,他们的页表映射标志位这里全都是只读的,所以我们在对代码区和字符常量区进行写入操作时,操作系统才能够拦截我们,我们写的时候进程才会挂掉。

(3) 页表条目还有一个标志位,来标识我们要访问的数据是否已经加载到内存当中

进程是可以被挂起的(挂起时我们如何知道进程的代码数据,在不在内存呢??)

我们先谈一个共识:现代操作系统,几乎不做任何浪费空间和浪费时间的事情

我们来讲一个例子:

相信大家都在电脑上打过游戏,不管是使命召唤,原神,还是其它的游戏,基本上都是几十个G,但我们的物理内存可能只有4GB,为什么我们的电脑能够运行呢?

其实是因为操作系统对大文件可以实现分批加载,在处理大文件时,一次性将整个文件加载到内存中可能会导致内存不足或性能下降。因此,操作系统可以采用分批加载的方法,将文件分成多个块,逐块读取和处理

我们知道了操作系统可以对大文件进行分批加载,可是如果我们加载了500M的空间,但是我们的代码还是一行一行的去跑的,也就是说,即便加载了500M的空间,但是我们短期之内可能只能用5M,那么我们就有一个问题,剩下的495MB,我们用不用把他们提前加载到内存呢?

我们把数据和代码扔到内存,但是由于进程调度,CPU配置,时间片等原因,这些代码和数据大概率是跑不完的,所以这些空间大概率还不会被使用,这就造成了把空间给你,但却没有被使用,这就违背了我们前面讲到的一个共识——现代操作系统,几乎不做任何浪费空间和浪费时间的事情。

所以操作系统对可执行程序加载的策略,实际上是一种惰性加载,也就是我们承诺给可执行程序这么大的空间,但是实际上在物理内存,几乎是用多少给多少。比如现在我们的游戏是2GB,我们可能就先给几kb,因为剩下的内存就算给了短期内也访问不到。本来这块内存还可以给其他进程的。这样内存的使用率就不高了。所以操作系统使用了惰性加载的方式

惰性加载:比如我们的正文代码段可能有500M,在外面的页表当中我们可以把虚拟地址全部填上,但是物理地址这边我们可以先只填一小部分。并且页表当中还有一个标记位,用来记录对应的代码和数据是否已经加载到内存。为0表示未加载,为1 表示已经加载。所以当我们在访问虚拟地址,就会查对应的页表上的标记位,看看对应的的代码和数据是否已经加载到内存,如果标志位为1,说明已经被加载,我们就直接读取物理地址直接访问。如果标志位为0,这个时候操作系统就需要触发一个概念——缺页中断,操作系统就会找到可执行程序,然后再在物理内存申请一块空间,把这个可执行程序剩下的代码和数据加载到这个物理内存里。然后把这段物理地址填到对应的页表当中,这样就实现边使用边加载这个时候,我们再来访问就可以访问到对应的代码和数据了。其实我们的写时拷贝也是缺页中断的原理。

现在我们来问一个问题,进程在被创建的时候,是先创建内核数据结构呢? 先加载对应的可执行程序呢??

学习了上面的原理,我们再来解答这个问题,进程在被创建的时候,一定是先创建内核数据结构的,把进程的PCB,地址空间,页表给维护起来,再来加载对应的内存(惰性加载)。

内存管理模块:我们前面讲到的如果我们要访问的虚拟地址对应的标志为0,要进行缺页中断,那么我们要在物理内存哪里申请空间,加载到物理内存上面地方,填写到页表上面地方,这一系列的过程就叫Linux的内存管理模块

那么我们这一系列的过程包括申请内存释放内存,填写页表等,我们的进程知道吗?我们进程进行调度、切换的时候,我们有没有管过内存怎么释放的,其实是不需要管的。正是因为有页表和地址空间的存在,所以我们的进程管理(进程怎么调度怎么切换)根本就不需要关心内存管理进程只需要使用虚拟地址,没有了内存管理自己会使用缺页中断,操作系统自己会调用内存管理的功能。所以,操作系统把进程管理和内存管理实现了软件层面上的解耦!!!

总结几个问题:

1、为什么要有进程地址空间?

(1)让进程以统一的视角看待内存 

如果没有进程地址空间和页表,进程的PCB在物理内存当中,代码和数据也只能在物理内存当中,所以进程就要记录自己的代码和数据在代码和数据在哪个地址处,从哪开始从哪结束,每个进程都需要这么做,而且每个进程直接使用物理内存了。这样进程就要对自己的代码和数据的使用情况进行管理工作。比如现在我们的进程因为阻塞被挂起了,代码和数据需要被换出,当他重新被换入的时候,他的代码和数据对应的物理地址可能已经变了,我们就需要重新修改PCB,这样的话太麻烦了,而有了进程地址空间和页表,我们就不需要关心我们的代码和数据在物理内存的什么位置。

有了进程地址空间和页表,每个进程都是通过进程地址空间和页表的映射,找到对应物理内存上的物理地址,这样的方式让每个进程以统一的视角看待内存。

(2)增加进程虚拟地址空间可以让我们访问内存的时候,增加一个转换的过程,在这个转化的过程中,可以对我们的寻址请求进行审查所以一旦异常访问,直接拦截,该请求不会到达物理内存,保护物理内存。

(3)因为有地址空间和页表的存在,将进程管理模块,和内存管理模块进行解耦合!

如果地址空间和页表的存在,我们进程直接进行物理内存的申请和释放,这种强耦合的代码一旦出现问题,那么内存管理出现问题,一定会影响进程调度。

2、进程切换

  • 我们对于进程就有了新的理解:进程=内核数据结构(task_struct&&进程地址空间(mm_struct)&&页表)+程序的代码和数据
  • 进程在进行切换的时候,不仅仅要切换PCB,也要切换地址空间,也要切换页表。只要切换了进程的PCB ,它所匹配的地址空间自动被切换,因为PCB指向对应的地址空间,又因为存放页表的地址的cr3寄存器属于进程的上下文,所以进程寄存器上下文只要已切换,那么页表自动被切换,所以归根结底,进程切换,我们只需要把CPU内的上下文一切换,那么PCB,地址空间,页表就全部切换了。所以我们每个进程就可以有这一整套东西了。    

3、进程具有独立性,怎么做到的?

  • 每个进程都有自己的PCB,地址空间,页表,所以在内存数据结构上,所有的进程是互相独立的。所以父子进程都有自己的内核数据结构
  • 还体现在在物理内存当中,我们加载到物理内存的代码和数据,不同进程虚拟地址可以完全一样,但是物理地址可以完全不一样,我们只需要让页表映射到物理内存的不同区域,这样每个进程的代码和数据就互相解耦了,即便是父子关系,只需要代码区一样,数据区不一样,也实现解耦了。所以我们的进程一旦异常,释放地址空间,释放PCB,释放页表,释放页表映射的物理内存,并不会影响其他进程。

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

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

相关文章

C++程序崩溃时,使用Windbg静态分析dump文件

Window环境下的C程序如果发生异常崩溃&#xff0c;首先会和客户联系&#xff0c;让帮忙取特定目录下的dump文件和log文件来分析崩溃的原因。不过发生崩溃的话&#xff0c;从log一般分析不出特定原因&#xff0c;这时候dump文件就起作用了。可以通过Visual Studio和WinDbg来静态…

看看顶级外贸业务员是如何跟进客户的?

许多外贸业务员&#xff0c;都碰到过客户跟进上的困难。那到底外贸业务员如何跟进客户&#xff0c;才能越跟越近&#xff0c;最终成交呢&#xff1f;本篇文章设定了几个客户回复的场景&#xff0c;看看顶尖销售都是怎么应对的吧&#xff1f; 一 客户说暂时没有需求 1.问清楚客…

京东app地推拉新和京喜大赢家网推app拉新申请渠道 附全套攻略

京东app地推拉新和京喜大赢家网推拉新可以通过“聚量推客”申请 聚量推客是什么呢&#xff1f;一手官签服务商直营app拉新平台 京东app地推拉新项目和京喜大赢家网推拉新项目有什么区别呢&#xff1f; 京东app只能地推作业&#xff0c;京喜大赢家推广支持网推推广操作也更简…

【C语法学习】18 - fread()函数

文章目录 1 函数原型2 参数3 返回值4 示例 1 函数原型 fread()&#xff1a;从与指定流stream相关联的二进制文件中读取数据块储存在str指向的内存空间中&#xff0c;函数原型如下&#xff1a; size_t fread(const void *ptr, size_t size, size_t count, FILE *stream)2 参数…

为什么大家都想找运营类的工作?

为什么大家都想找运营类的工作&#xff1f; 一、入门门槛 随着需求的持续增长&#xff0c;市场上的人员素质难以迅速匹配。在高校中&#xff0c;也难以培养出对口的应届毕业生。因此&#xff0c;许多企业在招聘运营人员时&#xff0c;更倾向于自行培养。对于新人的要求相对较低…

酒水展示预约小程序的效果如何

酒的需求度非常高&#xff0c;各种品牌、海量经销商组成了庞大市场&#xff0c;而在实际经营中&#xff0c;酒水品牌、经销商、门店经营者等环节往往也面临着品牌传播拓客引流难、产品展示预约订购难、营销难、销售渠道单一等痛点。 那么商家们应该怎样解决呢&#xff1f; 可以…

MTK RILD 无法启动问题分析

1、问题背景 Modem识别卡&#xff0c;但是UI无法识别卡&#xff0c;抓取问题机开机Log&#xff0c;发现RILJ一直在找寻RILD&#xff0c;但是无法获取相关服务&#xff0c;因此可以确认RILD Crash或出现异常。 但是在异常Log中搜索RILD&#xff0c;也未见相关关键字Log 2、对比实…

Springboot+vue的导师双选管理系统(有报告)。Javaee项目,springboot vue前后端分离项目。

演示视频&#xff1a; Springbootvue的导师双选管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot vue前后端分离项目。 项目介绍&#xff1a; 本文设计了一个基于Springbootvue的前后端分离的导师双选管理系统&#xff0c;采用M&#xff08;model&a…

享搭低代码平台:加速费用报销管理系统应用构建的利器

低代码平台的概念与优势 低代码平台是一种应用开发工具&#xff0c;它通过图形化界面和少量编码&#xff0c;使开发人员能够快速构建应用程序。它的优势包括提高开发效率、降低技术门槛、加强协作和灵活性等。 享搭低代码平台实现快速构建费用报销管理系统应用 在享搭低代码…

Web3.0的测试题

任务&#xff1a; 在前端开发一个查询UI&#xff0c;查询当前用户账户的ETH余额和指定ERC20合约中的余额 目标&#xff1a; UI框架指定使用 MUI (https://mui.com)需要查询到当前账户的ETH余额并展示在UI界面上需要输入ERC20合约地址后&#xff0c;查询到到当前账户在此ERC20…

【GEE】6、在 Google 地球引擎中构建各种遥感指数

1简介 在本模块中&#xff0c;我们将讨论以下概念&#xff1a; 如何在 GEE 中重命名图像的波段。如何使用已有的遥感指数。如何使用波段数学生成自己的遥感指数。 一个田地已经灌溉的年数的卫星图像。灌溉水最可能的来源是奥加拉拉含水层。图片来自科罗拉多州霍利奥克附近。资料…

测试开发路线大纲与总结

一、什么是测试开发&#xff1f; 测试开发&#xff08;Test Development&#xff09;是一种结合软件测试和软件开发的角色和实践。测试开发人员负责设计、开发和维护自动化测试工具、测试框架和测试脚本&#xff0c;以支持测试流程的自动化和优化。他们与开发团队和质量保证&a…

云尘靶场 Medium_Socnet 内网为docker 无站代理 不存在gcc的提权方式 解决ldd过高无法执行exp 指定so文件

首先我们可以通过 arp-scan 扫描当前内网中的存活 但是不知道为什么扫不出来 然后我们使用fscan可以获取存活 这里大致扫描只开了22端口 所以我们使用nmap进行信息收集扫描 nmap -sS -sV -A -p- 172.25.0.13通过tcp 进行 版本服务扫描 并且检测系统版本 全端口 这里我们可以…

VUE多语言i18n配置

1、i18n官网 格式化 | Vue I18n 2、安装i18n 8---指版本号 // vue2必须安装8版本的i18n包&#xff0c;vue3必须安装9版本的i18n包。 npm i vue-i18n8 3、卸载i18n npm uninstall vue-i18n 4、安装 js-cookie npm install vue-cookies --save 5、代码 5.1 main.js /…

睿思BI已支持3D图形

从睿思BI旗舰版V5.3开始&#xff0c;系统支持如下3D图形&#xff1a; 3D地球 3D地图 飞线图 3D金字塔 睿思BI采用ThreeJS实现3D功能&#xff0c;用户也可以基于系统接口&#xff0c;采用ThreeJS在数据大屏中实现自己的3D图形。 系统演示地址&#xff1a;睿思BI旗舰版https://…

BI数据可视化:不要重复做报表,只需更新数据

BI数据可视化是一种将大量数据转化为视觉形式的过程&#xff0c;使得用户可以更容易地理解和分析数据。然而&#xff0c;传统的报表制作过程往往需要手动操作&#xff0c;不仅耗时还容易出错。为了解决这个问题&#xff0c;BI数据可视化工具通常会提供一些自动化的数据更新功能…

台灯应该买什么样的才能护眼?一篇学会如果挑选护眼台灯

家里顶灯太暗了且高度太高&#xff0c;还是原始的LED灯&#xff0c;晚上用着眼睛都有点难受&#xff0c;还好遇到了儿童护眼灯。下面小编为大家介绍下儿童护眼灯哪个牌子好&#xff1f;什么护眼台灯比较专业 护眼台灯怎么样选择 1、照度级别 台灯照度级别分为 A 级和 AA 级。…

多模态情感分析——Twitter15和Twitter17数据集

一、原始数据集介绍 数据集链接&#xff1a; https://pan.baidu.com/s/1JLkaSerBgKe--GBaU0ZkFg?pwdfqyo提取码&#xff1a;fqyo 数据集介绍&#xff1a;原始的被划分为了训练集&#xff08;60%&#xff09;、验证集&#xff08;20%&#xff09;、测试集&#xff08;20%&am…

【Java SE】详解数组

前言&#xff1a;在C语言中我们已经学习过数组&#xff0c;接下来&#xff0c;我们再主要学习一下Java中的数组&#xff0c;在Java中&#xff0c;数组变得更加高效和使用。 1. 数组的基本概念 1.1 什么是数组&#xff1f; 数组&#xff1a;可以看成是相同类型元素的一个集合。…

【计算机网络】HTTP 协议

文章目录 前言什么是 HTTP理解 HTTP 请求和响应格式HTTP 的请求格式1. 首行2. 请求头3. 空行4. 正文&#xff08;body&#xff09; HTTP 的响应格式1. 首行2. 响应头3. 空行4. 正文&#xff08;body&#xff09; 首行GET 和 POST 方法有什么区别针对 GET 方法和 POST 方法的区别…