【Linux进程】进程地址空间

目录

程序地址空间回顾

进程地址空间

宏观理解

谈细节

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.rhkb.cn/news/182244.html

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

相关文章

axios 全局错误处理和请求取消

这两个功能都是用拦截器实现。 前景提要&#xff1a; ts 简易封装 axios&#xff0c;统一 API 实现在 config 中配置开关拦截器 全局错误处理 在构造函数中&#xff0c;添加一个响应拦截器即可。在构造函数中注册拦截器的好处是&#xff0c;无论怎么实例化封装类&#xff0c…

改进的yolov5

The networkstructure of these models is constant, but the modules and con-volution kernels are scaled, which alters the complexity and sizeof each model.&#xff08;这些模型的网络结构是恒定的&#xff0c;但模块和卷积核被缩放&#xff0c;这改变了每个模型的复杂…

node版本管理神器|nvm安装使用教程

文章目录 导文安装前提下载安装包进行安装第一步&#xff0c;下载安装包&#xff0c;[nvm下载地址](https://github.com/coreybutler/nvm-windows/releases)第二步&#xff0c;进行安装&#xff0c;点击nvm-setup.exe文件&#xff0c;接受用户协议第三步&#xff0c;选择安装目…

​Z时代时尚SUV新宠:起亚赛图斯值不值得年轻人买?

在当今汽车行业中&#xff0c;随着消费者偏好的多样化和年轻化&#xff0c;汽车制造商们正面临着前所未有的挑战与机遇。在2023年上海车展上&#xff0c;起亚汽车公司正式发布了全新紧凑级SUV——赛图斯。这款车型不仅标志着起亚对年轻消费市场的深入洞察&#xff0c;也展现了公…

Chatgpt批量改写文章网页版可多开软件-自动登录换号生成word或者TXT

Chatgpt批量改写文章网页版可多开软件介绍&#xff1a; 1、改写后生成docx格式文档和生成txt文档二选一。 2、支持原来docx文档里带图片的改写&#xff0c;改写伪原创后的docx里也带图片。 3、软件可以设置是否开启标题改写&#xff0c;可以自定义标题改写指令。 4、可以设置…

CVE-2023-34040 Kafka 反序列化RCE

漏洞描述 Spring Kafka 是 Spring Framework 生态系统中的一个模块&#xff0c;用于简化在 Spring 应用程序中集成 Apache Kafka 的过程&#xff0c;记录 (record) 指 Kafka 消息中的一条记录。 受影响版本中默认未对记录配置 ErrorHandlingDeserializer&#xff0c;当用户将容…

2023 现阶段H5的机型适配

个人愚见 现在的主流体验&#xff0c;是大屏手机展示更多的内容&#xff0c;并不着重于放大展示&#xff0c; 所以&#xff0c;外层布局使用vw,百分比&#xff0c;flex&#xff0c;内层直接px就行 比如微信服务页面&#xff0c;大屏下展示更多数据

海外服务器和国内的几大差别!如何选择一款优质的海外服务器?

随着互联网的普及&#xff0c;越来越多的企业或者个人都在搭建自己的网站&#xff0c;而对于搭建网站来说&#xff0c;选择一款合适的服务器显得至关重要。那么许多小伙伴可能都在纠结的一个问题是&#xff0c;海外服务器和国内服务器有什么区别&#xff1f;我们是否应该选择一…

http中的Content-Type类型

浏览器的Content-Type 最近在做web端下载的时候需要给前端返回一个二进制的流&#xff0c;需要在请求头中设置一个 writer.Header().Set("Content-Type", "application/octet-stream")那么http中的Content-Type有具体有哪些呢&#xff1f;他们具体的使用场…

消息中间件-RabbitMQ介绍

一、基础知识 1. 什么是RabbitMQ RabbitMQ是2007年发布&#xff0c;是一个在AMQP(高级消息队列协议)基础上完成的&#xff0c;简称MQ全称为Message Queue, 消息队列&#xff08;MQ&#xff09;是一种应用程序对应用程序的通信方法&#xff0c;由Erlang&#xff08;专门针对于大…

HarmonyOS 数据管理与应用数据持久化(二)

通过键值型数据库实现数据持久化 场景介绍 键值型数据库存储键值对形式的数据&#xff0c;当需要存储的数据没有复杂的关系模型&#xff0c;比如存储商品名称及对应价格、员工工号及今日是否已出勤等&#xff0c;由于数据复杂度低&#xff0c;更容易兼容不同数据库版本和设备…

win中安装nvm进行Node版本控制

之前有安装node.js安装包的需要先给卸载掉先通过官网下载安装包nvm-setup.zip nvm官网地址命令行打开输入nvm -v&#xff0c;测试安装是否成功 此时如果进行node相关版本安装的话&#xff0c;可能下载速度过慢&#xff0c;此时需要更换镜像源。 斜体样式找到nvm的安装路径 &am…

Java智慧工地管理平台可视化大数据建造工地APP源码

建筑行业是国民经济的重要物质生产部门和支柱产业之一&#xff0c;同时&#xff0c;建筑业也是一个安全事故多发的高危行业。如何加强施工现场安全管理、降低事故发生频率、杜绝各种违规操作和不文明施工、提高建筑工程质量&#xff0c;是摆在各级政府部门、施工企业面前的一道…

CRM软件如何高效培育销售线索?

​ 通过线索培育可以挖掘出更多CRM软件销售管道中的有价值客户提高销售业绩。但机遇与挑战总是共存的&#xff0c;培育线索要从不同的渠道执行大量重复性的操作&#xff0c;人为操控不仅速度慢而且容易出错&#xff0c;那么企业如何高效培育销售线索? 发送个性化邮件 我们知…

时间序列预测模型实战案例(八)(Informer)个人数据集、详细参数、代码实战讲解

论文地址->Informer论文地址PDF点击即可阅读 代码地址-> 论文官方代码地址点击即可跳转下载GIthub链接 本文介绍 本篇博客带大家看的是Informer模型进行时间序列预测的实战案例&#xff0c;它是在2019年被提出并在ICLR 2020上被评为Best Paper&#xff0c;可以说Inform…

5.网络之IP

IP协议&#xff08;网络层&#xff09; 文章目录 IP协议&#xff08;网络层&#xff09;1. 报文格式2. IP地址2. 地址管理3. 特殊IP地址 IP协议&#xff08;Internet Protocol&#xff0c;互联网协议&#xff09;&#xff0c;是TCP/IP协议栈中最核心的协议之一&#xff0c;通过…

wordpress版本识别

wordpress版本识别 1.通过RSS Feed识别 RSS Feed参考 访问网站/feed或者?feedrss 例如 默认结构&#xff1a;https://www.example.com/?feedrss2 其他结构&#xff1a;https://www.example.com/feed/ 返回结果中搜索 generator 可以看到直接是5.9.7版本 2.wpscan等工具扫一…

STM32 IIC 实验

1. 可以选择I2C1&#xff0c;也可以选择I2C2&#xff0c;或者同时选择&#xff0c;同时运行 配置时钟信号 为节约空间&#xff0c;选择这两个&#xff0c;然后选择GENERATE CODE 二、HAL_I2C_Mem_Write I2C_HandleTypeDef *hi2c&#xff1a;I2C设备句柄 uint16_t DevAddress&am…

Android Studio(RecyclerView)

前言 ListView的缺点&#xff0c;在RecyclerView得到了补充改善&#xff08;横纵向排列子元素、多列布局等等&#xff09; 代码 前面在适配器章节已经介绍了其对应的适配器&#xff0c;这里就简单展示一下多列布局的页面效果和相关代码 <androidx.recyclerview.widget.Recyc…

css-inpu边框

效果图&#xff1a; input {width: 225px;height: 25px;background-color: #1469bd00;border: #aca9a97d solid 1px;color: white;font-size: 15pt;box-sizing: conte-box; }input:focus {border-style: solid;border-color: #03a9f4;box-shadow: 0 0 15px #03a9f4;outline: …