[内核内存] 用户态进程虚拟内存管理

2024-05-25 09:48

本文主要是介绍[内核内存] 用户态进程虚拟内存管理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 1 linux 用户态进程虚拟地址空间
    • 1.1 arm32 用户进程的虚拟地址空间
    • 1.2 arm64架构用户态虚拟地址空间.
  • 2 linux用户态进程虚拟地址空间管理
    • 2.1 进程描述符task_struct
    • 2.2 进程用户态虚拟地址空间描述符
  • 3 用户态进程的虚拟地址和物理地址的映射管理
  • 4 linux用户态进程文件页的虚拟地址如何对应到磁盘中文件的具体位置?

1 linux 用户态进程虚拟地址空间

在linux多任务操作系统中,每个用户态进程都有自己的虚拟地址空间,用户态进程虚拟地址空间主要分内核虚拟地址空间和用户虚拟地址空间:

  • 内核虚拟地址空间:内核总是驻留在内存中,是操作系统的一部分。内核虚拟地址空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数

  • 用户虚拟地址空间

    NAMEDESCRIPTION
    栈(stack)局部变量、函数参数、返回地址等,由系统分配和统一回收
    内存映射段(mmap)内核将硬盘文件的内容直接映射到内存,是一种方便高效的文件I/O方式(mmap或用malloc分配大于128K的内存块)
    堆(heap)堆用于存放进程运行时动态分配的内存段,可动态扩张或缩减。堆中内容是匿名的(malloc分配小于128k内存块)
    BSS段未初始化数据
    data段已初始化的数据
    代码段(text)代码段也称正文段或文本段,通常用于存放程序执行代码(即CPU执行的机器指令),通常代码段是可共享的,因此频繁执行的程序只需要在内存中拥有一份拷贝即可

用户态进程地址空间中的虚拟地址需要通过页表(Page Table)的映射才能获取到其对应的物理地址,页表由操作系统维护并被处理器引用。其中进程内核地址空间中的虚拟地址需要通过内核态的页表才能定位到其映射的物理地址,而进程用户地址空间中的虚拟地址则需要通过进程自己私有的页表才能定位到其映射的物理地址.

在Linux中,内核地址空间的页表是持续存在的,并且所有进程切换到内核模式所处的内核地址空间都共用一套内核页表,因此内核代码和数据总是可寻址,随时准备处理中断和系统调用。与此相反,进程处于用户模式时所处的用户地址空间的映射关系随进程切换的发生而不断变化,因此每个进程的用户虚拟地址空间对应的页表是私有的。

新版本arm架构的linux系统用两个寄存器来储存一级页表(pgd页表)的虚拟地址:ttbr0和ttbr1.

  1. 对于arm32架构linux会将进程私有pgd页表的虚拟地址保存在ttbr0寄存器中,而ttbr1寄存器不会使用,因为ttb0和ttb1两个寄存器搭配使用的情况下linux只支持2G,1G和512M等,但是ARM32虚拟地址空间的划分比例为1:3,用户空间是3G,内核空间是1G,所以上述寄存器硬件限制无法满足这种通用配置,所以ARM32未使用TTBR1寄存器。那么当进程从用户态切换到内核态或内核态切换到用户态时,为了避免切换页表带来的性能损耗,arm32系统的用户态进程的用户虚拟地址空间和内核虚拟地址空间使用了相同的页表基地址,存储在ttbr0寄存器中。具体实现方式是: 其中内核空间的pgd页表内容会在进程被fork时复制到进程的私有pgd页表中,也就是说arm32架构的linux系统下用户态进程将linux内核态页表中的一级页表(init_mm->swapper_pg_dir)中的内容拷贝到了该进程的私有一级页表(task_struct->mm_struct->pgd)中,最终mmu只需通过一个ttbr0寄存器中存储的pgd页表就能完成内核态和用态两个虚拟地址空间中的虚实地址转换操作。
  2. 对于arm64架构linux会将内核虚拟地址空间pgd页表(init_mm->swapper_pg_dir)的虚拟地址放在ttbr1寄存器中,而用户进程私有pgd页表(task_struct->mm_struct->pgd)的虚拟地址存放在ttbr0中。当内核需要将获取该进程的一个虚拟地址vaddr对应的物理地址时,先会对该64位虚拟地址vaddr的最高位进行判断:
    1. 若vaddr对应的最高位为1,则可以确定该虚拟地址位于进程的内核地址空间,因此从ttbr1寄存器中获取到内核态pgd页表的地址数据,用于mmu完成对vaddr的地址映射操作.
    2. 若vaddr对应的最高位为0,则可以确定该虚拟地址位于进程的用户地址空间,因此从ttbr0寄存器中获取到该进程私有页表的对应的地址数据,用于mmu完成对vaddr的地址映射操作.

此处扩展下,就是对于x86架构linux os只用一个CR3寄存器来存储进程的页表信息,当进程被fork时内核虚拟地址空间对应pgd页表的内容回会被复制到用户态进程私有的pgd页表中去(处理方式类似arm32架构的os).

1.1 arm32 用户进程的虚拟地址空间

在linux多任务操作系统中,每个用户态进程都有自己的虚拟地址空间。在arm32架构的linux操作系统下用户态进程的虚拟地址空间是一个4G的内存地址块,通常用户态进程内核态和用态所占虚拟内存比例是1:3,该比例可以通过配置文件根据实际情况来进行设定.

START					    END               			SIZE			USE						
-----------------------------------------------------------------------------
0x00000000					0xbfffffff					3G			USER(用户虚拟地址空间)
0xC0000000					0xffffffff					1G			KERNEL(内核虚拟地址空间)ps:用户虚拟地址空间一般从ox0804800开始,前面空白部分为未使用地址空间

在这里插入图片描述

1.linux进程虚拟内存空间(32位)

1.2 arm64架构用户态虚拟地址空间.

对于arm64架构的linux,虽然虚拟地址已经达到64位,但是处理器的物理地址总线实际位宽并没有达到64位,通常为39位或48位,较新的arm架构可以支持到52位.

那么为什么arm64架构的的linux os物理地址总线位宽不支持到64位呢?因为物理地址总线宽度过高会给芯片设计带来较大难度,加之一个48位地址线宽,其寻址能力为256TB(2^48bytes),这对于目前的个人电脑或服务器都是够用的。综上所述,对于一个arm64架构的linux os往往用64位中的低48位虚拟地址来进行寻址(也可以通过内核配置进行调整可选项为38,48或52).

对于一个48位虚拟地址,4级页表,页面大小为4K的arm64架构linux os来说,其用户态进程的虚拟地址空间布局如下:

START					    END               			SIZE			USE						
-----------------------------------------------------------------------------
0x0000000000000000			0x0000ffffffffffff			256T			USER(用户虚拟地址空间)
0X0001000000000000			0Xfffeffffffffffff							非规范区
0xffff000000000000			0xffffffffffffffff			256T			KERNEL(内核虚拟地址空间)

其中用户进程地址空间的用户虚拟地址空间的内部结构划分情况和图1中arm32架构的结构划分基本一致,而对于内核虚拟地址空间的结构划分可参考以前介绍的linux内核内存初始化相关的介绍.

2 linux用户态进程虚拟地址空间管理

2.1 进程描述符task_struct

task_struct是linux内核的一种数据结构,Linux内核通过一个task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息。本文主要介绍进程用户空间虚拟内存管理,在task_struct中与进程虚拟地址空间相关的成员变量如下所示(内核态进程task_struct的mm成员为NULL):

//include/linux/sched.h
struct task_struct {....../**(1)struct mm_struct被称为进程描述符抽象并描述了Linux视角下管理进程地址空间的所有信息*(2)mm指向进程所拥有的内存描述符,而active_mm指向进程运行时所使用的内存描述符。*		a.对于普通进程,这两个指针变量相同*		b.对于内核线程,不拥有任何内存描述符,mm成员总是设为NULL;当内核线程运行时,它的active_mm成员被初始化		*		 为前一个运行进程的active_mm值*/struct mm_struct *mm, *active_mm;......
};

2.2 进程用户态虚拟地址空间描述符

进程用户态虚拟地址主要由如下两个数据结构来进行描述(只列出部分成员变量):

//include/linux/mm_types.h
struct mm_struct{//地址空间中所有VMA的链表首部struct vm_area_struct * mmap;rb_root_t mm_rb;//最后一次通过find_vma()找到的VMA存放处struct vm_area_struct * mmap_cache;//全局目录表的起始地址pgd_t * pgd;//访问用户空间部分的用户计数值atomic_t mm_users;//匿名用户计数值atomic_t mm_count;//正在被使用中的vma数量int map_count;//读写保护锁,长期有效struct semaphore mmap_sem;//用于保护mm_struct中大部分字段spinlock_t page_table_lock;//所有的mm_struct结构通过它链接在一起struct list_head mmlist;//代码段和数据段的起始地址和中止地址。unsigned long start_code, end_code, start_data, end_data//堆的起始地址和结束地址,栈的起始地址unsigned long start_brk, brk, start_stack;//命令行参数的起始地址和结束地址,环境变量区域的起始地址和结束地址。unsigned long arg_start, arg_end, env_start, env_end;/**rss:某一时刻,一般一个进程虚存空间不会完全在内存中,一般驻留在内存中的为其虚存空间的子集,rss描述有多少页驻		*留内存中)驻留集的大小是该进程常驻内存的页面数,不包括全局零页面,total_vm:进程中所有vma区域的内存空间总和,	  *locked_vm:内存中被锁住的常驻页面数*/unsigned long rss, total_vm,locked_vm;//VM_LOCKED用于指定在默认情况下将来所有的映射是上锁还是未锁。unsigned long def_flags;unsigned long cpu_vm_mask;unsigned long swap_cnt;//当换出整个进程时,页换出进程记录最后一次被换出的地址unsigned long swap_address;mm_context_t context;
}
//include/linux/mm_types.h
struct vm_area_struct {/* The first cache line has the info for VMA tree walking. *///指定VMA在进程虚拟地址空间的起始地址和结束地址unsigned long vm_start;		/* Our start address within vm_mm. */unsigned long vm_end;		/* The first byte after our end addresswithin vm_mm. *//* linked list of VM areas per task, sorted by address *///进程中所有的VMA都链接成一个链表struct vm_area_struct *vm_next, *vm_prev;//指定的VMA作为一个节点加入到红黑树中struct rb_node vm_rb;/** Largest free memory gap in bytes to the left of this VMA.* Either between this VMA and vma->vm_prev, or between one of the* VMAs below us in the VMA rbtree and its ->vm_prev. This helps* get_unmapped_area find a free area of the right size.*/unsigned long rb_subtree_gap;/* Second cache line starts here. *///指向该VMA所属进程的mm_struct结构体struct mm_struct *vm_mm;	/* The address space we belong to. *///VMA的访问权限pgprot_t vm_page_prot;		/* Access permissions of this VMA. *///指向该VMA的一组标志unsigned long vm_flags;		/* Flags, see mm.h. *//** For areas with an address space and backing store,* linkage into the address_space->i_mmap interval tree.*/struct {struct rb_node rb;unsigned long rb_subtree_last;} shared;//实现反向映射(anon_vma_chain,anon_vma)struct list_head anon_vma_chain; /* Serialized by mmap_sem &* page_table_lock */struct anon_vma *anon_vma;	/* Serialized by page_table_lock *//* Function pointers to deal with this struct. */const struct vm_operations_struct *vm_ops;/* Information about our backing store: *///指定文件映射的偏移量,单位是页面大小,对于匿名映射它的值是0或者vm_addr/PAGE_SIZEunsigned long vm_pgoff;		/* Offset (within vm_file) in PAGE_SIZE units *///描述一个被映射的文件,执行一个file实例struct file * vm_file;		/* File we map to (can be NULL). */void * vm_private_data;		/* was vm_pte (shared mem) */#ifndef CONFIG_MMUstruct vm_region *vm_region;	/* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMAstruct mempolicy *vm_policy;	/* NUMA policy for the VMA */
#endifstruct vm_userfaultfd_ctx vm_userfaultfd_ctx;
};

linux系统用户态一个进程虚拟地址空间由两个数据结构来描述mm_struct和vm_area_struct。mm_struct对进程整个用户空间虚拟内存进行了描述。同时一个进程的内存变化是动态扩充的,比如栈的扩充,堆的扩充,新文件的映射等,这些都需要一个粒度更小的结构体来表示内存的动态增加或减少,因此linux os又引入了vm_area_struct结构体来对进程的一段具有相同属性的虚拟地址空间进行描述。其中一个进程中的每个vm_area_struct实例都要连接到该进程mm_struct实例的mmap链表和mm_rb红黑树中,以方便查找。

最终linux进程的整个用户态虚拟地址空间都是通过mm_struct来进行描述,而虚拟地址空间上的每个段空间都是通过vm_area_struct来表示,如图2所示:

在这里插入图片描述

2.进程用户态虚拟内存管理

下面对task_struct,mm_struct和vm_area_struct这3者关系做一个总结:

linux内核为每一个进程维护一个task_struct结构体,内核用这个结构体来描述一个进程,这个结构包含了进程的一些信息,包括进程id,可执行文件名,指向用户栈的指针以及上下文的task结构体的指针等等。而task_struct中mm成员变量(该成员是一个mm_struct结构体数据)主要用于进程用户态虚拟地址空间的管理。这个mm_struct结构体中有两个比较重要的成员,一个是pgd,他指向该进程的页表集,便于根据虚拟地址查询页表获得需要的物理地址,另一个是mmap,他指向一个vm_area_struct的链式结构,链式结构中的每个vm_area_struct成员用来描述进程虚拟地址空间中具有相同属性的一个段,vm_area_struct结构体中包括该段的起止虚拟地址,标志访问权限的权限位,标志是否是共享区的标志位。CPU在操作一块数据时,内核会先遍历进程mm中的vm_area_struct链表(为了便于搜索,进程所有的vm_area_struct结构构成一个红黑树,树根保存在mm_struct中的mm_rb字段),若链表中任何一个VMA都与数据块的虚拟地址区间不匹配,就说明操作的地址还未分配,属于非法行为,内核会报段错误,并结束这个进程。当然如果权限不够也会报错。

ps:linux系统可以通过cat /proc/pid/map查看进程的虚拟地址段

3 用户态进程的虚拟地址和物理地址的映射管理

我们知道每个用户态进程都有一个独立的虚拟地址空间,维护着一个独立的页表。因此用户态进程虚拟地址vaddr必须与进程的唯一标识符进程pid相结合才有意义。用户态进程的虚拟地址和物理地址的映射如下所示 :

在这里插入图片描述

3.进程用户态虚拟地址映射示意图
  1. 通过进程的pid,获取到该进程的进程描述符task_struct结构体,进而通过task_struct结构体中的mm成员获得进程对应的虚拟地址空间描述符mm_struct结构体,最后通过mm_struct结构体成员pgd获得该进程的页表集。(ps:vaddr必须在mm_struct结构体的的mmap链表中找到对应的虚拟内存段vm_area_struct与其对应,否则会报段错误。此处默认vaddr在该进程虚拟地址空间范围内)

  2. 在步骤1获取到的进程页表集的基础上,将用户空间的虚拟地址vaddr通过MMU(pgd,pmd,pte)找到对应的页表项X(linux采用了一种与具体体系结构无关代码 的三层页表机制来完成内存管理,即使底层的体系结构并不支持这个概念。每一个进程都有一个指向其自己的PGD指针 (mm_struct->pgd),这就是一个物理页面号,其中包含了一个pgd_t类型的数组,进程页表的载入是通过把这个结构体复制到ttbr0寄存器完成。PGD表中每个有效的项都指向一个页面号,此页面号包含一个pmd_t类型的PMD项数组,每一个pmd_t又指向另外的页面号,这些页面号由 很多个pte_t类型的PTE构成,而pte_t最终指向包含真正用户数组的页面).

  3. 在找到页表项X后,需要对页表项X的数据进行分析来确定进程虚拟地址vaddr映射的物理页面相关情况。为了便于分析假设虚拟地址32位,页大小为4K。则页表项X为一个4bytes,32位的数据,其中X的12-31位是page base描述,而0-11位是属性描述。

    1. 若页表项X的0位为1,则进程的虚拟地址vaddr对应的物理页在物理内存中。则此时将页表项X的0-11位数据用虚拟地址的vaddr的0-11位数据替换,得到的32位地址即为虚拟地址vaddr对应的物理地址。(X[12:31]为虚拟地址vaddr所映射的物理页的页框号记为pfn,则vaddr对应的物理地址phyaddr = ( pfn<<12 ) & ( vaddr & ( 1 << 13 - 1) ) )

    2. 若页表项X的0位为0且X的1-31位都为0,则进程vaddr虚拟地址对应的页不在物理内存地址空间中,需要调页(缺页异常,触发缺页中断,以后专题讲解)。

    3. 若页表项的0位为0且1-31位至少有一位为1,则该页被交换到swap的磁盘分区。则此时页表项X的1-7位是表示的是磁盘交换区的区号,而X的8-31位表示磁盘交换分区的页槽索引(page-slot)类似于物理页的页框号(page frame)。由上我们就可以将该虚拟地址与磁盘的swap分区中的某一页关联起来。

      在这里插入图片描述

      4.pte页表项与swap磁盘分区索引图

4 linux用户态进程文件页的虚拟地址如何对应到磁盘中文件的具体位置?

在本章第3节中,我们根据某一进程的虚拟地址vaddr定位到了其pte页表项X,若该页表项X的0-31位都为0,则内存中没有物理页与vaddr虚拟地址建立映射关系,此时需要调页。假设vaddr虚拟地址映射的页是文件页(在进程mm_struct结构体中的mmap链表中找到vaddr对应的VMA结构体,若VMA中的vm_ops成员不为空,则vaddr映射的物理页为文件页,为空则vaddr映射的物理页为匿名页),对于文件页的调页linux需要进行如下操作,下面为了简化流程便于理解忽略page cache操作

  1. linux内核分配一个新的物理页A
  2. 将新物理页A的物理页框号(pfn)和页对应的权限信息更新到页表项X中并刷新进程的页表缓存
  3. 最后将虚拟地址vaddr文件页对应的磁盘文件内容copy到新的物理页A中,则文件页的调页过程完成(忽略page cache)。

上面这一过程其实就是缺页中断中linux对于文件页的一个处理过程。那么在上述的操作流程中linux内核是如何通过一个文件页的虚拟地址,准确地找到对应磁盘文件中的页数据的呢?

其实在用户进程P将文件映射到内存时,刚开始进程P只给文件分配了虚拟地址空间即分配了一个VMA(该VMA在进程页表集中也分配了对应的页表项,但对应的PTE页表项内容为空),而并没有将分配的VMA虚拟地址空间映射到物理内存中;但是在进程P在给文件分配VMA时,它会将磁盘文件的位置信息记录在VMA的vm_file和vm_pgoff成员中。假设进程P的虚拟地址vaddr在VMA的虚拟地址空间范围内,当对进程P的vaddr进行读或写操作时,会发现该vaddr虚拟地址并未映射物理地址;由此触发缺页异常,进程p因缺页中断而切换到内核态,进入内核态的进程p会先分偶尔一页物理内存并与vaddr建立映射(填写对应的pte页表项包括物理页的pfn和该页的访问权限信息)。接下来需要将vaddr对应的磁盘数据拷贝到新分配的物理页内存中,操作步骤如下:

  1. 在进程P用户态虚拟地址空间的vma链表中找到与vaddr对应的VMA
  2. 通过VMA中的vm_file,vm_pgoff成员和vaddr相对VMA起始虚拟地址的偏移offset这3个数据并利用VFS相关接口来定位到vaddr对应的文件段在磁盘中的具体位置。
  3. 最后通过相关系统调用函数将vaddr对应的文件段在磁盘中的数据拷贝到刚分配的物理页中。

以上就完成了文件页在缺页异常情况下的调页流程,需要注意的是上述流程中忽略了page cache操作。最后进程P就能通过虚地址vaddr获取到对应的文件数据。上诉流程分析结合图5更容易理解。

在这里插入图片描述

5.linux用户态进程文件页虚拟地址空间-物理地址空间-磁盘文件空间的映射关系示意图

ps:内存主要通过虚拟文件系统(VFS)来操作磁盘上的文件数据的,VMA的vm_file是一个struct file *数据结构,VFS系统能根据此数据结构确定该VMA虚拟地址空间对应于哪个磁盘文件。VMA的vm_pgoff记录的是文件的偏移量,VFS文件系统根据此数据确认该VMA虚拟地址空间对应磁盘文件中的哪一段数据。VFS详细介绍可参考https://www.cnblogs.com/huxiao-tee/p/4657851.html

知识来源:

1.https://www.zhihu.com/question/24011983

2.https://www.cnblogs.com/ck1020/p/6678530.html

3.https://blog.csdn.net/a372048518/article/details/103865898

这篇关于[内核内存] 用户态进程虚拟内存管理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1001164

相关文章

Linux内核之内核裁剪详解

《Linux内核之内核裁剪详解》Linux内核裁剪是通过移除不必要的功能和模块,调整配置参数来优化内核,以满足特定需求,裁剪的方法包括使用配置选项、模块化设计和优化配置参数,图形裁剪工具如makeme... 目录简介一、 裁剪的原因二、裁剪的方法三、图形裁剪工具四、操作说明五、make menuconfig

关于Java内存访问重排序的研究

《关于Java内存访问重排序的研究》文章主要介绍了重排序现象及其在多线程编程中的影响,包括内存可见性问题和Java内存模型中对重排序的规则... 目录什么是重排序重排序图解重排序实验as-if-serial语义内存访问重排序与内存可见性内存访问重排序与Java内存模型重排序示意表内存屏障内存屏障示意表Int

SpringBoot使用minio进行文件管理的流程步骤

《SpringBoot使用minio进行文件管理的流程步骤》MinIO是一个高性能的对象存储系统,兼容AmazonS3API,该软件设计用于处理非结构化数据,如图片、视频、日志文件以及备份数据等,本文... 目录一、拉取minio镜像二、创建配置文件和上传文件的目录三、启动容器四、浏览器登录 minio五、

如何测试计算机的内存是否存在问题? 判断电脑内存故障的多种方法

《如何测试计算机的内存是否存在问题?判断电脑内存故障的多种方法》内存是电脑中非常重要的组件之一,如果内存出现故障,可能会导致电脑出现各种问题,如蓝屏、死机、程序崩溃等,如何判断内存是否出现故障呢?下... 如果你的电脑是崩溃、冻结还是不稳定,那么它的内存可能有问题。要进行检查,你可以使用Windows 11

如何安装HWE内核? Ubuntu安装hwe内核解决硬件太新的问题

《如何安装HWE内核?Ubuntu安装hwe内核解决硬件太新的问题》今天的主角就是hwe内核(hardwareenablementkernel),一般安装的Ubuntu都是初始内核,不能很好地支... 对于追求系统稳定性,又想充分利用最新硬件特性的 Ubuntu 用户来说,HWEXBQgUbdlna(Har

IDEA中的Kafka管理神器详解

《IDEA中的Kafka管理神器详解》这款基于IDEA插件实现的Kafka管理工具,能够在本地IDE环境中直接运行,简化了设置流程,为开发者提供了更加紧密集成、高效且直观的Kafka操作体验... 目录免安装:IDEA中的Kafka管理神器!简介安装必要的插件创建 Kafka 连接第一步:创建连接第二步:选

C#如何优雅地取消进程的执行之Cancellation详解

《C#如何优雅地取消进程的执行之Cancellation详解》本文介绍了.NET框架中的取消协作模型,包括CancellationToken的使用、取消请求的发送和接收、以及如何处理取消事件... 目录概述与取消线程相关的类型代码举例操作取消vs对象取消监听并响应取消请求轮询监听通过回调注册进行监听使用Wa

NameNode内存生产配置

Hadoop2.x 系列,配置 NameNode 内存 NameNode 内存默认 2000m ,如果服务器内存 4G , NameNode 内存可以配置 3g 。在 hadoop-env.sh 文件中配置如下。 HADOOP_NAMENODE_OPTS=-Xmx3072m Hadoop3.x 系列,配置 Nam

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖