虚拟内存以及进程的虚拟内存分布(第六章)

您所在的位置:网站首页 云上太康客户端怎么下载软件 虚拟内存以及进程的虚拟内存分布(第六章)

虚拟内存以及进程的虚拟内存分布(第六章)

2024-07-11 21:51:55| 来源: 网络整理| 查看: 265

在早期的计算机中,程序都是直接运行在物理内存上的,意思是运行时访问的地址都是物理地址,而这要求程序使用的内存空间不超过物理内存的大小。

在现代计算机操作系统中,为了提高CPU的利用率计算机同时运行多个程序,为了将计算机上有限的物理内存分配给多个程序使用,并做到隔离各个程序的地址空间和提高内存利用率,操作系统应用虚拟内存机制来管理内存。

本文介绍的是一些与虚拟内存相关的概念,包括虚拟内存和物理内存之间的映射,一个进程的虚拟内存空间的划分等。  

目录

1.物理内存 vs 虚拟内存

2.物理内存空间 和 虚拟内存空间

3.4GB虚拟内存

cpu位宽 vs cpu的地址总线位宽

4.虚拟内存的地址空间划分

1)Windows 系统下— NULL 指针区+用户区+ 64KB 禁入区+内核区

1)NULL指针区和64KB禁入区:略

2)用户区每个进程私有使用的,称为用户地址空间。

3)内核区是所有进程共享的,称为系统地址空间。

2)Linux下和Windows下的差不多——内核空间,用户空间和保留区

1.保留区:

2.可执行文件映像,堆,栈,动态库

3) 详细介绍下栈空间(Stack)——函数调用:

通过例子1看汇编:

例子2:(VC9,i386,Debug模式)

PS1:编译器 生成的函数 的 标准进入和退出指令序列 

PS2:编译器 实现的hook技术

PS3:函数调用之调用惯例

PS4:函数调用之返回值的传递

PS5:函数调用之C++对象

4)堆空间(heap)——动态申请内存:

5.虚拟地址和物理地址的映射

6.物理内存和硬盘之间的置换

7.虚拟内存的重要性

8.进程的虚存空间分布——装载(《程序员的自我修养-链接装载库》第6.4节)

readelf -S链接视图——25个section头

执行视图:7个program头——程序头表记录着程序头

VMA

Stack VMA[stack]

动态链接时的进程堆栈初始化信息

9.windows打开任务管理器

内存项含义

1.工作集(内存)Working Set = 内存(专用工作集)+ 内存(共享工作集)【第2列=第3列+第4列】

2.提交大小 Comitted Memory——进程独占的内存

3.PROCESS_MEMORY_COUNTERS  类 和 GetProcessMemoryInfo 函数

2 其他项目含义

cpu时间是cpu在这个进程用的总时间。

磁盘有关的问题:

分析内存问题:

其他观察进程的exe

10.硬件概念--存储器芯片。

文章比较长,也比较杂。可以分次阅读。【修改记录:2023.5.6修改看了1)】

1)1,2,3,4(排除栈空间函数调用的例子),5,6,7

2)8

3)9,10

3)4中栈空间函数调用的例子

1.物理内存 vs 虚拟内存

物理内存就是内存条,实实在在的内存,即RAM。

虚拟内存是内存管理中的一个概念,是操作系统管理每个进程的内存空间的一个概念。

对于一个进程来说,虚拟内存是进程运行时所有内存空间的总和,包括共享的,非共享的、存在物理内存中,存在分页内存中(分页后面会介绍)、提交的,未提交的。(上面这些不是互斥的概念哈,注意标点符号)

【进程运行起来以后,虚拟内存映射=物理空间(PP)+硬盘空间(DP)+未使用使用映射的。】

【硬盘空间(DP):虚拟内存映射的可能有一部分不在物理内存中,在其他介质中,比如硬盘。举个例子,当你的程序需要创建一个1G的数据区,但是此时剩余500M的可用物理内存了,那么此时势必不是所有数据都能一起加载到内存(物理内存)中,势必有一部分数据要放到其他介质中(比如硬盘),待进程需要访问那部分在其他介质中的数据时,再通过调度进入物理内存。】

ps:有些概念不懂,继续看就行,后面都会讲到一些,就慢慢理解了。

2.物理内存空间 和 虚拟内存空间

怎么标识每个内存位置呢,用地址来表示。

物理内存大小组成的地址空间就叫物理内存空间。

物理内存空间表示的是实实在在的RAM物理内存,其地址空间可以看成一个由 M 个连续的字节大小的单元组成的数组,每个字节都有一个唯一的物理地址。

(其实,还有一些io设备会映射到物理空间,但是这里就先忽略了)

【存储单元,也就是每个字节都有其地址,cpu进行访问的时候需要知道其地址。M就是RAM的大小】

虚拟内存大小组成的地址空间就叫虚拟内存空间。

跟物理内存一样,也是有地址空间的,用地址标识哪个内存位置,也看成一个连续的字节大小的单元组成的数组。

【虚拟内存空间实际上并不存在的,需要的只是虚拟内存空间到物理内存空间的映射关系的一种数据结构。当然有时候并不是都映射到物理内存中,前面说过还有其他介质中】

上面说的数组的大小,就是地址空间的长度。

即地址空间是一个抽象的概念,可以想象成一个很大的数组,每个数组的元素是一个字节(就是一个地址,存一个字节的内容),而这个数组的大小就是由地址空间的地址长度决定。一般画图也是这么画的。

比如16G的物理内存条,具有0~0x3FFFFFFFF(16G)的地址长度的寻址能力。

【另一方面,cpu的地址总线位宽决定了可以直接进行寻址物理内存空间最大值】

4G虚拟内存,具有0~0xFFFFFFFF的地址长度的寻址能力。(为什么举例4GB虚拟内存,因为每个进程的虚拟内存空间是4GB,32位操作系统中)

在一个系统中,物理内存空间只有一个,但是虚拟内存空间有很多个(运行着多个进程)。每个虚拟内存空间都有必须有一个映射关系对应于物理内存。

【在进程启动的时候会建议一个映射关系,在运行中维护这个关系,后面会讲】

对于一般程序而言,虚拟内存空间中的很大部分的都是未使用的,【虚拟内存是4G的空间】

每个虚拟内存空间往往只能映射到很少一部分物理空间上。

每个物理页(将整个物理空间划分成多个大小相等的页)可能只是被映射到一个虚拟地址空间中,也有可能存在一个物理页被映射到多个虚拟地址空间中,那么这些地址空间将共享此页面(这时,如果在一个虚拟地址空间改写了此页面的数据,这在其他的虚拟地址空间也可以看到变化)

3.4GB虚拟内存

操作系统(32位)会为每一个新创建的进程分配一个 4GB 大小的虚拟内存,从0到2^32-1。

(这里说的分配4GB的虚拟内存并不是分配4GB的空间,而是创建一个映射表。

映射表的设计有一级页表和二级页表结构。

这个映射表肯定加载在物理内存中,理想上,这个映射表是设计得当然是越小越好了,因为每个进程都需要这么一个映射表)

之前一直说是4G的虚拟地址空间,那么为什么是分配一个4GB的虚拟地址空间,因为在32位操作系统下一个32位的程序的一个指针长度是 4 字节,

(指针即为地址,指针里面存储的数值被解释成为内存里的一个地址。64位程序指针是64位,寻址能力2^64-1)

那么4 字节指针(地址)的寻址能力是从 0x00000000~0xFFFFFFFF ,最大值 0xFFFFFFFF 表示的即为 4GB 大小的容量。

那么这个虚拟空间地址大小 当然也是和 实际物理内存RAM大小没有关系的。

一个进程的虚拟内存的大小是由操作系统的位宽和程序的位数(是32位还是64位)决定的。

64位的cpu可以支持32位的操作系统,也可以支持64位的操作系统。

扯一个别的:cpu位宽和cpu地址总线位宽。

cpu位宽 vs cpu的地址总线位宽

cpu位宽不等于cpu的地址总线位宽

16位cpu(cpu位宽是16位)的地址总线位宽可以是20位

32位cpu的地址总线位宽可以是36位

64位cpu的地址总线位宽可以是36位或者40位(cpu能够寻址的物理地址空间为64GB或者1T)。

1.我们说的16位cpu,32位cpu,64位cpu,指的都是cpu的位宽,表示的是一次能够处理的数据宽度,即一个时钟周期内cpu能处理的2进制位数,即分别是16bit,32bit和64bit。不是地址总线的数目。

(那么是谁决定了cpu可以处理的数据宽度呢?通用寄存器的宽度决定了cpu可以直接表示的数据范围。我们说的16位cpu,32位cpu,64位cpu,指的就是通用寄存器的位数(宽度)。见 汇编语言 那篇文章)

2.cpu的地址总线位宽决定了可以直接进行寻址物理内存空间。

所以如果cpu的址总线位宽是32位的,也就是它可以寻址能力就是0~0xFFFFFFFF(4G)的物理内存空间。(36位或者40位,它们寻址的物理地址空间为64GB或者1T)

虽然如果你的计算机上只装了512M的内存条,物理地址的有效部分只有0x00000000~0x1FFFFFFF,其他部分都是无效的物理地址。

(这里无视一些外部的I/O设备映射到物理空间。)

在cpu访问任何存储单元必须知道其物理地址,所以在一定程度上,cpu的地址总线宽度影响了最大支持的物理内存RAM大小。

32位系统最大只能支持4GB内存之由来 - Matrix海子 - 博客园

4.虚拟内存的地址空间划分 1)Windows 系统下— NULL 指针区+用户区+ 64KB 禁入区+内核区

为了高效的调用和执行操作系统的各种服务,Windows会把操作系统的内核数据和代码(内核提供了各种服务供应用程序使用)映射到所有进程的进程空间中。即内核态,也称为系统空间,也可以叫做系统空间。

所以内核态只有一个,会映射到所有进程的虚拟内存空间中。从这个角度来看,用户空间是每个进程独立的,内核空间是共享的。

下面这两个图可以看的比较清楚。来自《软件调试 卷2 windows平台调试》,如果没记错的话。

1)NULL指针区和64KB禁入区:略 2)用户区每个进程私有使用的,称为用户地址空间。

用户区存放的是程序代码和数据, 堆, 共享库, 栈。

默认的32位windows配置下是2GB+2GB,称为2GB模式。可以修改windows配置,可以设置3GB用户地址空间+1GB的系统地址空间,称为3GB模式。

(但是现在64位操作系统,是一个更大的数字了,具体不造) ,

如上图:(以下都是用户态的,用户态的,用户态的,ntdll.dll是用户态的,而且都是共享,每个进程都是同一个虚拟地址的。一些固定的地址。)

0x80000000附近的:

如系统库(

ntdll.dll:

windows操作系统用户层的最底层的dll,(从上上个图中也可以看出来)

负责windows子系统与windows内核之间接口,比如堆管理器的核心接口,HeapCreate、HeapAlloc、HeapFree和HeapDestroy,向操作系统申请内存的时候WindowsAPI——VirtualAlloc进行申请。

ntdll.dll是NT内核派驻到用户空间的领事馆

ntdll.dll是用户态和内核态沟通的桥梁。

用户空间的代码通过这个dll,来调用内核空间的系统服务。

操作系统会在启动阶段将这个加载到内存中,并把他映射到进程空间的同一个虚拟地址。

0x10000000:如运行时库(msvcr90d.dll,microsoft的c运行时库,运行时库这这篇文章介绍过)

0x00400000:可执行程序映像exe,即程序代码和数据(为什么叫映像,在这个文章介绍过)

stack栈的位置:在0x00300000和exe后面都有分布。因为有多少个线程就有多少个栈,线程默认栈大小1MB,也可以启动线程的时候自行设定大小。(所以栈,是相对于线程来说的,这在调试的时候可能看出来)

heap堆的位置:在剩下的空间中进行分配。

3)内核区是所有进程共享的,称为系统地址空间。

内核区保存的是系统线程调度、内存管理、设备驱动等数据,这部分数据供所有的进程共享以及操作系统的使用——程序在运行的时候处于操作系统的监管下,监管进程的虚拟空间,当进程进行非法访问时强制结束程序。

(进程只能使用操作系统分配给进程的虚拟空间。错误提示“进程因非法操作需要关闭”就是访问了未经允许的地址。)

2)Linux下和Windows下的差不多——内核空间,用户空间和保留区

1.保留区:

对内存中收到保护而禁止访问的内存区域的总称。在大多数操作系统中,极小的地址都是不允许访问的,如果访问了就会抛出错误。如NULL。 通常C语言将无效指针赋值为0,因为0地址上正常情况下不可能有有效的可访问的数据的。

2.可执行文件映像,堆,栈,动态库

1)可执行文件映像:装载的时候涉及到。可执行文件的装载,进程和线程,运行时库的入口函数(第六章)_u012138730的专栏-CSDN博客_运行时动态装载链接至少需要用到以下哪些函数

2)堆:应用程序动态分配的 通常在栈的下方。地址从低到高分配。

3)动态链接库(共享库)映射区。动态链接的时候介绍。Linux之前是默认从0x4000000开始装载的,但是在linux内核2.6中,共享库的装载地址已经挪到了靠近栈的位置,即0xbfxxxxxx附近。。

4)栈:用于维护函数调用的上下文。有函数调用就要用到栈。地址从高到低分配。

3) 详细介绍下栈空间(Stack)——函数调用:

栈是虚拟内存空间的一段连续的地址。里面的内容是 调用函数的活动记录,记录目前为止函数调用之前的那些函数的维护信息。

(函数调用,维护信息,堆栈帧,活动记录)

一个函数的活动记录,ebp 和 esp 划定范围,如下图所示:

(其中一个函数的参数和函数返回地址用ebp+x 可以得到):

ebp和esp都是寄存器,详情看汇编指令和寄存器_u012138730的专栏-CSDN博客。

以上就是一个堆栈帧

bp指向当前的堆栈帧的底部

sp指向当前的栈帧的顶部。

随着函数调用的进行以及返回,bp和sp也就是随着改变,指向新的堆栈帧或者旧的堆栈帧。

如上图,一条活动记录包括一下几个方面:

1)函数的返回地址(地址 ebp + 4)和函数的参数(地址 ebp + 8,ebp + 12等等)、

如果对应到汇编指令中,

函数的返回地址是在call指令中push 的

函数的输入实参是在call之前的指令压入的

2)调用该函数之前的ebp的值(旧ebp值,上一个堆栈帧的底部的地址),目的是为了:这个函数返回的时候ebp的值可以恢复到上一个位置,回到上一个堆栈帧现场。3)【可选】需要保持不变的寄存器的值(地址 ebp - 4,ebp - 8等等),也就是当进入函数时,会push进去一些需要保存的寄存器的值,等函数退出之前再pop出来。4)临时数据,局部变量,调试信息——扩大的栈空间中。

通过例子1看汇编:

SimpleSection.c中的func1

看func1函数的反汇编的实现(这里是.AT&T的汇编格式,看文章的最后):——其实就是创建了一个堆栈帧的过程,从ebp开始,不包括输入实参和返回地址。

前面几句的含义写到了图片上,接下去几句:

movl $0x0,(%esp) 的含义:

在esp指针寄存器指向的位置存入0x0(其实代表的是第一个参数“%d\n”,那为什是0呢,因为要重定位。这条指令就是printf函数的第一个实参,上一条指令就是printf的第二个参数参数i。相当于从右到左压入实参)

(所以printf的两个输入参数就分别存在当前堆栈帧的esp+4 和 esp 中,下面一条语句就是call了再次进入函数调用了,所以这就是之前说的,输入实参是在call之前压入的。)

可以看到.text段(代码段)的offset是0x10的地方正好是指令movl $0x0,(%esp) 中0x0的地址。(上上个图中数数,第16个字节)

需要重定位的符号是.rodata,可以看到.rodata里存的正好就是%d。

【.rodata, .data, .bss应该也是链接的时候重定位,跟普通的符号printf和func1一样,因为链接完了以后这些段的VMA已经确定了,就可以填上正确的值了。这里看的是目标文件.o,还没有重定位过】

call 15的含义:

15,就是跳转到func1+0x15 这里的的内存的地址进行执行,call指令做了两件事情:

1)把当前指令的下一条指令的地址压入栈中,即函数的返回地址。

2)进入新的函数调用了(新的函数开头都是,重复开头的ebp入栈,ebp=esp等等,像进入func1一样的——进入printf的例行操作)

leave 的含义:(相当于执行了出栈的操作,把栈恢复到函数调用以前的样子)

等价于下面两条指令:

movl %ebp %esp  恢复esp=ebp的值,即此时esp指向的是ebp的位置,就一开始的时候那样。

popl %ebp 从栈中弹出值,其实就是旧的ebp的值,赋值给ebp寄存器,即ebp = 旧ebp。弹出旧ebp以后此时esp指向的就是函数的返回地址那个位置了。对应上面活动记录的图。

ret 的含义:

等效于以下汇编指令:

popl %eip  从栈中取出函数的返回地址,并跳转到该位置执行

这个例子中没有push寄存器的值,退出的时候也就不需要pop回来。

例子2:(VC9,i386,Debug模式) int foo() {        return 123; }

下面是汇编:(第四步,在debug模式下会加入调试信息,把栈空间上的内容都初始化为CC,两个连续排列的CC就是中文“烫”)

例子1是AT&T汇编格式,例子2是Intel汇编格式。

比例子1多了第3,4,6步。

以下的ps可以不用看,跟本章内容无关只是做个记录。                              

PS1:编译器 生成的函数 的 标准进入和退出指令序列 

不知道不懂蓝色划线的原因。

PS2:编译器 实现的hook技术

钩子技术:

下面是可被替换的函数(FUNCTION)的进入指令序列:

nop指令占用一个字节,一共5个字节的nop:

比如一替换函数(REPLACEMENT_FUNCTION)如下:

替换以后如下:

进入到Function以后,先执行了jmp LABEL(这个jump是近跳指令,只占用两个字节,覆盖原来的mov edi,edi)

LABEL下是jmp 到一个新的标签即替换函数(这个jmp占5个字节,,覆盖原来nop的五个字节)

这样就实现了函数的替换了。

说实话,没有很懂,具体实际应用中的流程。

PS3:函数调用之调用惯例

函数声明的时候可以用关键字声明调用惯例,默认是cdecl(C语言中):

可以看到调用惯例规定了 :

出栈方:出栈可以是调用方,也可是函数本身(将函数实参压入栈的肯定是调用方)。上面例子中的leave就是调用方执行的。参数传递:即调用方将实参压入栈 的 顺序 需要和函数本身 有一致的规定,这样才能取到正确的值。名字修饰:不同的调用惯例有不同的名字修饰规则,所以,如果不一致的话,就会找不到符号了

不同的编译器对同一种调用惯例的实现也不尽相同,比如gcc和vc对于C++的thiscall(p298)

调用惯例调用方和被调用方不一致的例子:p299 ,要在动态链接中说明如何链接成功,这里先略了。

例子:

main函数:1 2-调用方对函数参数进行压栈,从右到左,4-调用方对函数参数进行出栈。

f函数也是:栈在调用完以后都栈都恢复到原来的调用之前

vs中设置默认调用惯例:

 

cdecl调用惯例的用途——p337变长参数。

常用的调用约定类型有__cdecl、stdcall、PASCAL、fastcall。除了fastcall可以支持以寄存器的方式来传递函数参数外,其他的都是通过堆栈的方式来传递函数参数的。(这是网上看到的,不是书里面写的)

PS4:函数调用之返回值的传递

上面的——例子2:(VC9,i386,Debug模式)——中可以看到返回值是由寄存器eax来传递的。但是如果返回值大于4个字节,不能存放在eax寄存器中应该怎么办呢——解决办法是:eax指向一个地址,返回值就存放在这个这个地址中。下面是返回是类的例子,其中 big_thing 大小为 128个字节。

typedef struct big_thing { char buf[128]; }big_thing; big_thing return_test() { big_thing b; b.buf[0] = 0; return b; } int main() { big_thing n = return_test(); return 0; }

main函数的汇编:

int main() { 01041720  push        ebp   01041721  mov         ebp,esp  01041723  sub         esp,258h   ---》开辟258h的栈空间 01041729  push        ebx   0104172A  push        esi   0104172B  push        edi  0104172C  lea         edi,[ebp-258h]   01041732  mov         ecx,96h   01041737  mov         eax,0CCCCCCCCh   0104173C  rep stos    dword ptr es:[edi]   ----》把栈空间都初始化为cch,即汉字烫烫烫。。。 96h*4=258h。就是栈空间的大小。

    big_thing n = return_test();0104173E  lea         eax,[ebp-254h]  ---》lea指令看文章汇编指令和寄存器_u012138730的专栏-CSDN博客01041744  push        eax   ----》eax值是栈空间的最后一个地址,把eax压入栈,紧接着下面就调用call,说明把这个当作了输入参数 return_test(ebp-254h) 01041745  call        return_test (010410E1h)  0104174A  add         esp,4  --》函数调用方负责把压栈参数还原0104174D  mov         ecx,20h   01041752  mov         esi,eax   01041754  lea         edi,[ebp-1CCh]   0104175A  rep movs    dword ptr es:[edi],dword ptr [esi]  ---》把eax的内容复制到ebp-1CCh中,ebp-1CCh是栈上的一个临时的地址。20h*4=80h 就是正好128字节就是big_thing的大小。rep movs指令汇编指令和寄存器_u012138730的专栏-CSDN博客 0104175C  mov         ecx,20h   01041761  lea         esi,[ebp-1CCh]   01041767  lea         edi,[n]   0104176D  rep movs    dword ptr es:[edi],dword ptr [esi]  ---》再把临时的地址的内容复制到真正的n中。     return 0; 0104176F  xor         eax,eax   }

return_test的汇编

big_thing return_test() { 01041690  push        ebp   01041691  mov         ebp,esp   01041693  sub         esp,148h  ---》开辟148h的栈空间 01041699  push        ebx   0104169A  push        esi   0104169B  push        edi   0104169C  lea         edi,[ebp-148h]   010416A2  mov         ecx,52h   010416A7  mov         eax,0CCCCCCCCh   010416AC  rep stos    dword ptr es:[edi]   ---》初始化148h的栈空间  rep stos  指令见文章 汇编指令和寄存器_u012138730的专栏-CSDN博客     big_thing b;     b.buf[0] = 0; 010416AE  mov         eax,1   010416B3  imul        ecx,eax,0   010416B6  mov         byte ptr b[ecx],0  ---》假汇编 b其实是栈空间的一个地址      return b;010416BE  mov         ecx,20h   010416C3  lea         esi,[b]   010416C9  mov         edi,dword ptr [ebp+8]  ---》ebp+8就是之前main函数调用return_test时,压入了一个作为隐形参数出入到return_test中的,在main函数的栈的地址。 (数据应该是存入 [旧的ebp-254h]的内存地址中了 )010416CC  rep movs    dword ptr es:[edi],dword ptr [esi]  ---》把b的内容复制到ebp+8中。 010416CE  mov         eax,dword ptr [ebp+8]  ---》把epb+8中存的地址复制给eax,也就是main函数的中的栈空间的某个地址,也就是返回值。 }

但是如果return_test返回类型太大,main中的栈空间也无法满足要求,那么就是会使用一个临时的栈上的内存作为中转,返回值对象就会被拷贝2次。

即如果是函数的返回值大于4字节,调用的时候相当于多传入一个输入参数——一个指针,函数里面的返回值指向传入的这个指针。这个指针赋值给eax。返回以后,调用方通过这个指针获取到真正的返回值来进行使用。

PS5:函数调用之C++对象 #include using namespace std; struct cpp_obj { cpp_obj() { cout


【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭