Linux——进程地址空间与进程控制

进程地址空间与进程控制

文章目录

  • 进程地址空间与进程控制
  • 1. 进程地址空间
    • 1.1 进程地址空间的引入
    • 1.1 进程地址空间的特点
    • 1.2 页表
    • 1.3 C/C++的地址
    • 1.4 进程地址空间 + 页表的优势
  • 2. 进程控制
    • 2.1 进程创建
      • 2.1.1 写时拷贝
    • 2.2 进程终止
      • 2.2.1 进程退出码
      • 2.2.2 异常信号码
      • 2.2.3 errno
      • 2.2.4 总结
    • 2.3 进程等待
      • 2.3.1 wait()
      • 2.3.2 形参*status
      • 2.3.3 waitpid()

本章思维导图:
在这里插入图片描述注:本章思维导图对应的 .xmind.png文件都已同步导入至 资源

1. 进程地址空间

在这里插入图片描述

1.1 进程地址空间的引入

以前我们可能看过如下图类似的不同数据的地址分布图:

在这里插入图片描述

我们可以通过打印部分数据的地址来验证上图的正确性:

#include <stdio.h>int a;
int b = 1;void Func(){};int main()
{printf("main = %p\n", main);printf("Func = %p\n", Func);printf("&a = %p\n", &a);printf("&b = %p\n", &b);return 0;
}

output:

main = 0x40050e
Func = 0x400507
&a = 0x601034
&b = 0x60102c

即地址:正文代码 < 初始化数据 < 未初始化数据

示例二:

#include <stdio.h>
#include <stdlib.h>int main()
{
int a[2];
int A[2];
int* b = (int*)malloc(sizeof(int) * 2);
int* B = (int*)malloc(sizeof(int) * 2);for (int i = 0; i < 2; i++)printf("a[%d] = %p\n", i, &a[i]);printf("\n");for (int i = 0; i < 2; i++)printf("b[%d] = %p\n", i, &b[i]);printf("\n");for (int i = 0; i < 2; i++)printf("A[%d] = %p\n", i, &A[i]);printf("\n");for (int i = 0; i < 2; i++)printf("B[%d] = %p\n", i, &B[i]);return 0;
}

output:

a[0] = 0x7ffd9342fc38
a[1] = 0x7ffd9342fc3cb[0] = 0x1944010
b[1] = 0x1944014A[0] = 0x7ffd9342fc30
A[1] = 0x7ffd9342fc34B[0] = 0x1944030
B[1] = 0x1944034

可以得出两个结论:

  • 栈空间地址高于堆空间
  • 栈空间的地址是从高到低减小的,堆空间的地址是从低到高增大的

示例三:

重点来了

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t id = fork();  //利用系统调用fork()创建一个子进程
int num = 10;if (id == 0)
{num = 0;printf("子进程,num = %d, address = %p\n", num, &num);exit(-1);
}printf("父进程,num = %d, adderss = %p\n", num, &num);return 0;
}

output:

父进程,num = 10, adderss = 0x7ffca5330fb8
子进程,num = 0, address = 0x7ffca5330fb8

大家应该发现了两个奇怪的现象:

  1. 拥有相同名字的变量num,为什么可以同时拥有两个不同的值10和0呢?
  2. 假设父进程的num和子进程的num不同,那么两个不同的变量为什么可以有相同的地址0x7ffca5330fb8

对于第一个问题,我们首先给出结论:父进程的num和子进程的num确实不是同一个变量。我们将在进程控制一节进行详细的说明

对于第二个问题:

  • 首先我们应该清楚,每一个变量都对应着一块独一无二的内存地址(物理内存)。换句话说,对于物理内存,不同变量的地址是绝对不同的
  • 因此,对于示例三两个不同的变量却有相同地址的情况,我们可以得出结论:%p打印出的地址绝对不是物理地址
  • 不是物理地址,那是什么?这种地址就是我们这一节要讨论的虚拟地址
  • 我们最上面放出的图不是物理内存,而是进程地址空间

在这里插入图片描述

1.1 进程地址空间的特点

在这里插入图片描述

  • Linux中,每个进程都认为自己独占内存,因此每个进程都有一个地址空间,也就是进程地址空间,在32位系统中,这块地址空间的大小为整个内存的大小即4GB(232bit)
  • 进程地址空间并不是真正的内存,因此并不会真正存储代码和数据,可以认为他只是一张描述进程占有资源的一张表。进程的代码和数据实际上还是存储在物理内存中
  • 进程地址空间也是一种资源,因此也要被操作系统管理。
    • 如何管理?先描述再组织,因此描述进程地址空间的各种信息都被组织在了一个结构体mm_struct中,而mm_struct同时作为描述一个进程的信息,其会被封装在进程的task_struct
    • 同时,为了对应到上图各种地址的分布,mm_struct一定有区域的划分,即用变量记录了各个区域的起始地址和结束地址

现在问题来了:

既然虚拟地址并不存放代码和数据,那我们是通过什么来找到一个虚拟地址对应的物理地址呢

答案是页表

1.2 页表

在这里插入图片描述

  • 每个进程都有独自的进程地址空间,那自然也要有独自的页表
  • 页表存放着虚拟地址和物理地址的映射关系,这样,虚拟地址就可以通过页表的映射来找到对应的物理内存
  • 页表不仅有虚拟地址和物理地址的关系,同时还记录着对应数据的权限信息
    • 例如,为什么字符串常量不能修改?就是因为其存放的虚拟地址区域在页表的权限为只读,因此当要对这块空间进行写操作时,操作系统就会根据页表的权限终止对物理内存的映射,也就无法改变常量的内容了。

1.3 C/C++的地址

在引入部分的示例三中,我们通过%p打印出的地址实际上是虚拟地址,实际上C/C++用到的地址全都是虚拟地址

既然如此,那我们便会对malloc和new动态开辟内存有更深入的理解:

  • malloc、new申请的空间实际上都是虚拟地址的空间,一开始并没有实际申请物理内存
  • 当要对开辟的空间进行写操作时,就会进行缺页中断
    • 由于虚拟地址并不会实际存放代码和数据,因此首先要先申请物理内存的空间并通过页表构建映射关系,最后才能进行写入

这样做有两个好处:

  • 可以提高malloc、new申请空间的效率(一开始并不要申请物理内存并构建映射关系)
  • 可以防止空转,节省资源,提高空间利用率(申请的空间可能并不会使用)

1.4 进程地址空间 + 页表的优势

在这里插入图片描述

将物理内存从无序变有序,让进程以统一的视角看待内存。并让动态管理内存成为可能

  • 物理内存通常由多个内存块组成,其并没有固定的顺序;而虚拟地址是连续且有序的进程地址空间,因此可以通过页表的映射关系,让物理内存从无序变有序
  • 进程可以通过虚拟地址空间+页表这种方式统一看待内存
  • 由于虚拟地址空间是连续且有序的,因此也可以很方便的开辟并释放空间,并通过页表的映射实现动态内存管理,并提高内存利用率
    • 当要动态开辟数据时,可以通过页表映射到一块空闲的内存区域
    • 当释放掉一块动态开辟的空间后,可以将这块空间重新映射到其他进程的虚拟地址空间

将进程管理和内存管理进行解耦合

虚拟地址空间并不存放实际的代码和数据,因此可以在不改变代码和数据的前提下动态管理内存

是保护内存的重要手段

  • 由于页表存储着相关数据的权限信息,因此当要进行非法访问时,操作系统会根据这个权限直接终止
  • 如果进程都是在物理内存直接开辟,那么当进行内存的动态开辟时,可能会和其相邻的进程空间产生影响,导致进程的不连续

2. 进程控制

2.1 进程创建

在这里插入图片描述

需要包含头文件<unistd.h>

Linux通过**系统调用forK()**创建子进程

函数原型:

pid_t fork(void);
  • 该函数没有形参
  • 返回类型pid_t实际上就是短整形short
  • 如果创建子进程成功,那就给子进程返回0,给父进程返回子进程的`PID
  • 如果子进程创建失败,那么就会给父进程返回-1

子进程以父进程为模板创建,和父进程共享代码和数据。如图所示:

在这里插入图片描述

知道了这一点后,我们就可以回答一个问题:

为什么fork()函数可以有两个返回值呢,或者说为什么它可以return两次?

这是因为:

  • fork()是一个创建子进程的函数,因此在它的函数体内**return之前,他就已经产生了子进程**
  • 而子进程和父进程共享代码和数据,因此return这一条语句也就会分别对父进程和子进程执行
  • 所以,fork()看起来可以return两次,实际上时父进程return了一次,子进程return了一次

我们也可以用一份示例代码来验证上面说到的结论:

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>int main()
{pid_t id = fork();if (id == 0){while (1){printf("I am child process\n");sleep(1);}}while (1){printf("I am father process\n");sleep(1);}return 0;
}

运行效果:

在这里插入图片描述

可以发现,子进程和父进程是同时运行的

2.1.1 写时拷贝

当子进程或者父进程要向存储的数据进行写操作时,由于进程具有独立性并且进程创建时子进程和父进程共享代码和数据,因此为了让子进程和父进程的数据不会相互影响,就会触发写时拷贝

写时拷贝:

  • 即为在写的时候拷贝数据
  • 当要向子进程和父进程共享的数据进行写入时,会先对这份数据进行一次拷贝,并通过页表映射到新的空间,生成一个副本,最后再对这个副本进行写操作
  • 这样,就可以确保子进程和父进程的数据不会相互影响,从而确保了进程的独立性

如图所示:

在这里插入图片描述

这时小伙伴就有几个疑问了:

为什么要在写的时候才拷贝呢?为什么不直接拷贝呢?

  • 应该清楚,虽然子进程和父进程共享代码和数据,但是我们并不一定会会对这些数据进行写操作
  • 因此,只在写的时候才拷贝可以有效地节省空间以及创建进程的时间

为什么是拷贝,而不是申请一块一样大小的内存?

  • 就拿对数组这种数据进行写操作来说,假设我们只改变数组某一个下标的元素,而不是改变整个数组
  • 那么如果只是申请一块空间,就不能知道其他未修改部分的内容
  • 因此考虑到只是对数据做部分覆盖(修改)的情况,必须对源数据进行拷贝,生成一个副本

写时拷贝的原理是什么?

  • 当子进程被创建完成后,页表会将父子进程共享的代码和数据的权限设置为只读

  • 当对这些数据进行写操作时,就会触发错误,从而引导操作系统的介入,触发写时拷贝

这样,我们就算对写时拷贝有了一个较为清楚的认识,同样我们也能回答当初遗留的问题:

代码pid_t id = fork()id为什么可以有两个值?

  • 前面就已经说过,fork()在return前(也就是id接收返回值前)就已经创建了子进程,因此id就已经是父子进程共享的数据
  • 当要对id进行写操作时,就会触发写时拷贝,就会生成一个副本id
  • 因此,父子进程就会同时拥有变量名相同但是实际的物理地址不同的变量id
  • 所以看起来id有两个值,实际上就是父进程的id有一个值,子进程的id有一个值

2.2 进程终止

在这里插入图片描述

首先需要清楚,进程终止有且只有三种情况:

  1. 代码执行完,且结果正确
  2. 代码执行完,且结果错误
  3. 代码未执行完,发生异常

2.2.1 进程退出码

进程退出码用来描述代码执行完,结果的正确情况

例如:

int main() {return 0;}

里面的return 0中的0就是进程退出码。

  • 对于进程退出码,0代表执行成功,非0代表执行失败
  • 每一个进程退出码实际上都对应着一个具体的执行情况,我们可以用库函数strerror进行查看
    • 头文件<string.h>
    • 函数原型:char *strerror(int errnum);

示例代码:

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>int main()
{int i;for (i = 0; i < 200; i++)printf("%d: %s\n", i, strerror(i));return 0;
}

output:

在这里插入图片描述

可以看到,在Linux系统中,一共有134中进程退出码

在命令行中,我们也可以通过命令来得到最近一次进程的错误返回码

echo &?

在这里插入图片描述

2.2.2 异常信号码

异常信号码用来描述代码为执行完,出现异常的情况

我们可以用命令查看所有的异常信号码以及对应的信息

kill -l

在这里插入图片描述

我们也可以用命令向指定的进程发送信号:

kill -num PID

2.2.3 errno

errno是一个是一个整形变量,使用时需要包含头文件<errno.h>

  • errno可以用来记录最近一次系统调用或者库函数的执行情况,如果成功,errno为0,否则为对应的错误码
  • 每一次系统调用或库函数调用都会刷新一次errno

例如:

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>int main()
{FILE* fp = fopen("data.txt", "r");	//data.txt为一个不能存在的文件if (NULL == fp){printf("%s\n", strerror(errno));exit(-1);}return 0;
}

output:

No such file or directory

2.2.4 总结

通过上面的讲解,我们知道,要知道代码是否出异常,就需要知道它的异常信号码,要知道它的运行结果是否正确,就需要他的进程退出码。因此,如果想要知道一个进程的执行情况,就一定需要两个整数:进程退出码和异常信号码

2.3 进程等待

在这里插入图片描述

我们之前提到过:

如果子进程先于父进程退出,但是父进程没有等待子进程,拿走它留下的资源,那么这个子进程就会变成僵尸进程。从而造成内存泄露等不良后果。

所以在子进程退出后,我们必须进行进程的等待。如何等待?我们可以利用系统调用wait()waitpid()。下面进行详细的说明:

要调用这两个系统调用,需要包含头文件:

<sys/wait.h><sys/types.h>

先来看wait()

2.3.1 wait()

pid_t wait(int *status);
  • wait()会暂停调用进程(父进程)的执行,直到其任意一个子进程终止。换句话说就是:只有等待到任意一个子进程终止,父进程才会继续工作
  • 如果成功,则返回被等待的子进程的PID;否则返回-1
  • *status为一个输出型参数。用来表示被等待的子进程的执行情况,如果不关心可以设置为NULL

实例:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{pid_t id = fork();if (id == 0){int cnt = 5;while (cnt--){printf("I am child process, PID = %d\n", getpid());sleep(1);}printf("child process exit\n");exit(-1);}sleep(5);printf("father will wait child 5 seconds later\n");sleep(5);wait(NULL);printf("wait sucess!!!\n");while(1);return 0;
}

效果:

在这里插入图片描述

2.3.2 形参*status

我们前面说过,*status是一个输出型参数,表示被等待子进程的执行状态,如果我们不关心,可以设置为NULL

但是如果我们要关心呢?简单,用一个整型变量接收即可。

例如:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>int main()
{pid_t id = fork();if (id == 0){exit(-1);}int status = 0;wait(&status);printf("status = %d\n", status);return 0;
}

output:

status = 65280

这时,有小伙伴就纳闷了:65280这个数是什么意思,我们要怎么分析这个数呢?

我们前面提到过,如果想要准确地描述一个进程的执行状态,必须要两个这个整数:进程退出码和异常信号码

因此,既然status可以表示一个进程的执行状态,那它也一定包含了这两个数的信息

  • 实际上,作为一个32位的int型数据,它的每一位都被赋予了特定的信息,我们应该将其当作一个位图来看待:

在这里插入图片描述

  • 因此,我们就可以利用位运算来提取一个进程的退出码和信号码:
exit_code = (status >> 8) & 0xff;
sign_code = status & 0x7f
  • 例如对于上面的status = 65280这种情况,65280的二进制形式为:1111 1111 0000 0000。我们取它的前8位:1111 1111,将其转换为原码:1000 0001也就是子进程的退出码-1

  • 同样,如果子进程被信号所杀,我们也可以得到对应的异常信号码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>int main()
{pid_t id = fork();if (id == 0){while(1);exit(-1);}int status = 0;wait(&status);printf("status = %d, exit_code = %d, sign_code = %d\n", status, (status >> 8) & 0xff, status & 0x7f);return 0;
}

效果:

在这里插入图片描述

有些小伙伴可能会觉得要获取一个进程的退出码和信号码每次都要写一个位运算会很麻烦。所以我们也可以用系统定义的宏来完成:

WIFEXITED(status);	//如果进程正常退出,就返回true
WEXITSTATUS(status);	//代表进程的退出码WIFSIGNALED(status);	//如果进程由信号终止,就返回true
WTERMSIG(status);	//代表进程的信号码

例如:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>int main()
{pid_t id = fork();if (id == 0){exit(1);}int status = 0;wait(&status);if (WIFEXITED(status))printf("exit_code = %d\n", WEXITSTATUS(status));if (WIFSIGNALED(status))printf("sign_code = %d\n", WTERMSIG(status));return 0;
}

output:

exit_code = 1

2.3.3 waitpid()

pid_t waitpid(pid_t pid, int *status, int options);

关于pid我们讨论两种情况

  • pid == -1:表示等待任何一个子进程
  • pid > 0:表示等待PID == pid的子进程

status和上面说的一样,这里不再赘述

关于options这里也讨论两种情况

  • options == 0。此时,waitpid(-1, NULL, 0)就和wait(NULL)完全等价。在这种情况下,父进程会进行阻塞等待,如果一直没有子进程退出,那就会一直等待下去。
  • options == WNOHANG。在这种情况下,父进程就会进行非阻塞等待,即如果在调用该系统调用的时候,如果没有子进程退出,就会立即返回,而不会被卡住。如果是这种情况,waitpid()的返回值也有以下三种情况:
    • 返回值大于0,表示等待成功
    • 返回值等于0,表示没有子进程退出
    • 返回值等于-1,表示发生错误

所以,我们可以利用waitpid的非阻塞等待方式进行基于非阻塞的轮询访问

我们可以将系统调用waitpid()放入循环体中,不断进行对子进程的等待,同时也可以在等待的间隙做父进程需要做的事情

例如:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>typedef void(*func)();func task[3];void Func1()  {printf("Func1\n");}
void Func2()  {printf("Func2\n");}
void Func3()  {printf("Func3\n");}void taskInit()
{
task[0] = Func1;
task[1] = Func2;
task[2] = Func3;
}void excuteTask()
{
for (int i = 0; i < 3; i++)task[i]();}int main()
{
taskInit();pid_t id = fork();if (id == 0)
{int cnt = 5;while (cnt--){printf("I am child process, PID = %d\n", getpid());sleep(1);}printf("child process exit\n");exit(1);
}while (1)
{if (waitpid(id, NULL, WNOHANG) > 0){printf("wait success\n");break;}excuteTask();sleep(1);
}return 0;
}

效果:

在这里插入图片描述

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

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

相关文章

前端对接 —— 周末

1.点击校验 点击校验 宇哥 记得过滤 不能校验的数据&#xff08;我后端还要检验吗&#xff1f;&#xff09; 2.前端数据对接 这个可以吗&#xff1f; 这种的可以吗&#xff1f;

go原生http开发简易blog(一)项目简介与搭建

文章目录 一、项目简介二、项目搭建前置知识三、首页- - -前端文件与后端结构体定义四、配置文件加载五、构造假数据- - -显示首页内容 代码地址&#xff1a;https://gitee.com/lymgoforIT/goblog 一、项目简介 使用Go原生http开发一个简易的博客系统&#xff0c;包含一下功能…

使用堆排序算法

使用堆排序算法&#xff08;升序&#xff0c;建大堆顶&#xff09;完善此程序。&#xff08;注&#xff1a;截图为堆排序初始化后的大堆顶结果&#xff0c;详见源代码&#xff09; #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #define N 10void input(int a[], …

【react.js + hooks】基于事件机制的跨组件数据共享

跨组件通信和数据共享不是一件容易的事&#xff0c;如果通过 prop 一层层传递&#xff0c;太繁琐&#xff0c;而且仅适用于从上到下的数据传递&#xff1b;建立一个全局的状态 Store&#xff0c;每个数据可能两三个组件间需要使用&#xff0c;其他地方用不着&#xff0c;挂那么…

MySQL日志管理,备份与恢复

备份的主要目的是灾难恢复&#xff0c;备份还可以测试应用、回滚数据修改、查询历史数据、审计等。 而备份、恢复中&#xff0c;日志起到了很重要的作用 MySQL日志管理是数据库管理中的一个重要方面&#xff0c;它可以用于诊断问题、监控性能、进行故障恢复等。MySQL主要有几种…

【合成数字】合成类游戏-uniapp项目开发流程详解

以前玩过2048游戏&#xff0c;从中发现规律&#xff0c;想到跟合成类游戏相似&#xff0c;知道为什么很相似吗&#xff0c;在这里&#xff0c;做一个数字合成游戏玩玩吧&#xff0c;感兴趣的话可以看看&#xff0c;这里给大家讲一讲数字合成游戏的开发过程。 文章目录 创建项目…

Pandas-DataFtame的索引与切片(第3讲)

Pandas-DataFtame的索引与切片(第3讲)         🍹博主 侯小啾 感谢您的支持与信赖。☀️ 🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ�…

【动态读取配置文件】ParameterTool读取带环境的配置信息

不同环境Flink配置信息是不同的&#xff0c;为了区分不同环境的配置文件&#xff0c;使用ParameterTool工具读取带有环境的配置文件信息 区分环境的配置文件 三个配置文件&#xff1a; flink.properties&#xff1a;决定那个配置文件生效 flink-dev.properties&#xff1a;测…

主持知识竞赛类节目的一般流程是什么

竞争性的团队活动&#xff0c;更适合青年的特点&#xff0c;更容易得到青年的支持&#xff0c;也是"离教于乐"的好方式。 这类活动可从内容和特点上分为知识性竞赛&#xff0c;技能性竞赛&#xff0c;文娱性竞赛&#xff0c;体育竞技性竞赛等形式。 知识性竞赛&…

【VMware安装及虚拟机配置】

1. 下载VMware 进入 VMware Workstation 17 Pro下载链接 下拉到如下位置&#xff0c;点击DOWNLOAD 2. 安装VMware 参考&#xff1a;虚拟机VMware下载与安装教程 本次安装是vmware 17&#xff0c;安装步骤差不多&#xff0c;只参考第二部分即可。 3. 激活VMware 密钥&…

【Qt QML入门】TextInput

TextInput&#xff1a;单行文本输入框。 TextInput除了光标和文本外&#xff0c;默认没有边框等效果。 import QtQuick import QtQuick.Window import QtQuick.ControlsWindow {id: winwidth: 800height: 600visible: truetitle: qsTr("Hello World")//单行文本输…

HarmonyOS开发实战:如何实现一个运动排名榜页面

HarmonyOS开发实战&#xff1a;如何实现一个运动排名榜页面 代码仓库&#xff1a; 运动排名榜页面 项目介绍 本项目使用声明式语法和组件化基础知识&#xff0c;搭建一个可刷新的排行榜页面。在排行榜页面中&#xff0c;使用循环渲染控制语法来实现列表数据渲染&#xff0c;…

03 使用Vite开发Vue3项目

概述 要使用vite创建Vue3项目&#xff0c;有很多种方式&#xff0c;如果使用命令&#xff0c;则推荐如下命令&#xff1a; # 使用nvm将nodejs的版本切换到20 nvm use 20# 全局安装yarn npm install -g yarn# 使用yarnvite创建项目 yarn create vite不过&#xff0c;笔者更推荐…

LeedCode刷题---滑动窗口问题(二)

顾得泉&#xff1a;个人主页 个人专栏&#xff1a;《Linux操作系统》 《C/C》 《LeedCode刷题》 键盘敲烂&#xff0c;年薪百万&#xff01; 一、将X减到0的最小操作数 题目链接&#xff1a;将 x 减到 0 的最小操作数 题目描述 给你一个整数数组 nums 和一个整数 x 。每一…

mysql的负向条件查询会不会使用索引

mysql的负向条件查询&#xff0c;例如not in&#xff0c;会不会使用索引&#xff1f; 其实&#xff0c;mysql还是会尽量利用索引。如果查询的列上有索引&#xff0c;并且索引能够覆盖查询所需的列&#xff0c;那么mysql可能会使用索引来获取结果&#xff0c;而不是进行全表扫描…

2024中国国际大数据产业博览会年度主题征集公告

2024中国国际大数据产业博览会年度主题征集公告 中国国际大数据产业博览会&#xff08;以下简称数博会&#xff09;&#xff0c;是全球首个以大数据为主题的国家级博览会&#xff0c;由国家发展和改革委员会、工业和信息化部、国家互联网信息办公室和贵州省人民政府共同主办&am…

ADB命令安装卸载手机APP

前言 手机内置的浏览器很多广告&#xff0c;推荐的新闻也很多负面的新闻&#xff0c;所以就想卸载内置的手机app&#xff0c;不过现在很多手机都是限制了内置的软件都不能卸载&#xff0c;以前随便获取一下root权限&#xff0c;也是可以卸载的&#xff0c;不过最近搞了一下&am…

【POI的如何做大文件的写入】

&#x1f513;POI如何做大文件的写入 &#x1f3c6;文件和POI之间的区别是什么&#xff1f;&#x1f3c6;POI对于当今的社会发展有何重要性&#xff1f;&#x1f3c6;POI大文件的写入&#x1f396;️使用XSSF写入文件&#x1f396;️使用SXSSFWorkbook写入文件&#x1f396;️对…

设计可编辑表格组件

前言 什么是可编辑表格呢&#xff1f;简单来说就是在一个表格里面进行表单操作&#xff0c;执行增删改查。这在一些后台管理系统中是尤为常见的。 今天我们根据vue2 element-ui来设计一个表单表格组件。&#xff08;不涉及完整代码&#xff0c;想要使用完整功能可以看底部连…

ReenterLock重入锁

synchronized就是一种最简单的控制方法&#xff0c;它决定了一个线程释放可以访问临界区资源。 同时&#xff0c;Object.wait()方法和Object.notify()方法起到了线程等待和通知的作用。 ReenterLock重入锁可以完全替代关键字Synchoronized.重入锁是Synchoronized、Object.wait(…