ptmalloc:从内存虚拟化说起

前言

本文并不局限于ptmalloc的原理,而是从linux的内存虚拟化和系统调用原理出发,结合各种语言实现,讲明内存分配方面的trade off,力图事无巨细,追根究底。本文内容包括但不限于:NIO原理、0拷贝原理、内存虚拟化、GC和内存分配、PCB结构、mmap原理和场景、JVM内存分配细节、缺页异常中断、PTE、物理页分配、驻留内存、malloc分配原理、ptmalloc的设计和缺陷、mimalloc设计。

什么是glibc和ptmalloc

glibc提供了一组在所有Linux发行版上都可用的标准化函数。包括ISO C standard library、POSIX实现、内存管理等。其中的内存分配函数ptmalloc2被包括C++、JVM(Native Heap)、Python在内广泛使用。
ptmalloc2遵循malloc函数的惯例:小内存使用brk分配,大内存使用mmap分配。
同时,ptmalloc2也因为它的内存碎片、内存泄漏和线程间锁争用问题而广受诟病,因此谷歌推出了tcmalloc,facebook推出jemalloc,微软推出mimalloc来取代它。一般在生产环境也建议根据不同的情况选用不同的内存分配库替换掉glibc的实现。

思考题1:ptmalloc2的问题会对哪些语言造成影响?为什么?

答案:
对C++、JVM(Native Heap)、Python都有影响。但是对JVM影响较小。
要理解这个问题首先要理解JVM的内存分配。
首先我们看JVM的内存划分:
image.png
主流GC的中,GC要么是Partial GC要么是Full GC:

  • Partial GC:并不收集整个GC堆的模式
    • Young GC:只收集young gen的GC
    • Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
    • Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
  • Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。

所以内存回收的粒度是比较大的,对于mimalloc并非像C++和Python那么敏感。这个影响跟G1的-XX:MaxGCPAuseMillS参数也有关系。如果这个jvm参数设置的太低的话,就会导致每次G1 回收的内存很少,受到的底层回收算法影响更大。
更多的关于GC的细节,可以参考下面的简单聊聊GC场景下的内存释放。
那么什么时候知道glibc对JVM产生了影响呢?通过RSS(进程驻留内存)来看:
image.png

内存增长模型

虚拟地址空间由地址总线宽度决定:

64位linux进程虚拟地址空间的内存增长模型如下所示:
image.png
思考题2: NIO bytebuffer分配的内存属于堆内内存还是堆外内存?属于user space还是kernal space?

答案:既可能是堆内内存也可能是堆外内存,既可能属于user space也可能属于kernal space。
image.png

虚拟地址空间、brk和mmap

分配小于128k内存时虚拟地址空间的情况
image.png
分配内存大于128k时的情况:
image.png
brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的,这就是内存碎片产生的原因),而mmap分配的内存可以单独释放。
mmap函数的作用就是分配/映射一段虚拟地址空间:
image.png

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系(写时复制)。
mmap能解决那些性能问题?

  1. 用户态到内核态的过程中,内存拷贝问题
  2. 内核态把脏数据写回到块设备的过程中,内存拷贝的问题
  3. 4K对齐问题
  4. 零拷贝(实际上是通过映射)问题


通过下面的流程图可以很直观的明白mmap的作用:
image.png
image.png

思考题:

  1. swap space大小影响虚拟空间地址大小吗?
  2. malloc调用的mmap会设置flags为MAP_ANONYMOUS吗?也就是说malloc会映射到文件吗?
  3. mmap什么时候回写到文件?
  4. mmap在文件映射时导致的脏页在回写之后怎么再次标记为脏页触发回写?为什么跟write不一样?
  5. mmap在文件映射时物理内存+swap spcae不够怎么办?
  6. mmap怎么保证文件页缓存一致性?

答案:

  1. 不会
  2. 不会
  3. image.png

dirty pages的物理页可以通过address_space 中的Radix树快速找到并且被pdflush回写。
image.png
struct mm_struct
struct mm_struct 包含所有与进程相关的内存区域。 The mm field of struct task_struct is a pointer to the struct mm_struct of the current process.
struct vm_area_struct
A struct vm_area_struct is created at each mmap() call issued from user space. A driver that supports the mmap() operation must complete and initialize the associated struct vm_area_struct. The most important fields of this structure are:

  • vm_start, vm_end - the beginning and the end of the memory area, respectively (these fields also appear in /proc//maps);
  • vm_file - the pointer to the associated file structure (if any);
  • vm_pgoff - the offset of the area within the file;
  • vm_flags - a set of flags;
  • vm_ops - a set of working functions for this area
  • vm_next, vm_prev - the areas of the same process are chained by a list structure

TCB结构图
image.png

  1. 回写的页会变成写保护,写会再次触发缺页

PTE中R/W位标志是否写保护
image.png
为什么跟write不一样?我们先看看write的过程:
image.png
根本原因当然是再次写不能切换到内核态无法修改PTE,只能通过写保护再次触发写时复制缺页异常,标记为脏页。

  1. mmap触发的缺页异常并不会一次将所有文件内容读到内存中。

linux将缺页异常分为几种情况分别分配内存,包括PTE是否为空,是匿名映射还是文件映射、是读文件还是写文件、页面是否换出、是否满足COW等等情况。
如果是文件映射导致的缺页异常,最终的调用是这样的:

	//如果map_pages函数不为空并且fault_around_bytes有效,//map_pages就是之前讲过的预读的操作函数,fault_around_bytes控制预读长度,一般64kif (vma->vm_ops->map_pages && fault_around_bytes >> PAGE_SHIFT > 1) {//调用do_fault_around预读几个页的文件内容读取到vmf->page,为了减少页错误异常的次数ret = do_fault_around(vmf);if (ret)return ret;}

可以通过MADV_SEQUENTIAL来更激进地申请mmap内存,也可以通过MAP_POPULATE直接将文件全部加载到内存中,这也意味着内存会被更快的释放。

  1. 内核态申请的PTE是共享的 PTE中的G位表示是否共享

ptmalloc

image.png
memory
应用地址空间,由RAM或swap提供
chunk
可以在应用中分配、在glibc中释放或与相邻chunk组合成较大范围的小范围内存。请注意,chunk是给定给应用的memory的包装器。每个chunk存在于一个heap中,属于一个arena。
heap
memory中的一个连续区域,它被细分为要分配的chunk。每个heap恰好属于一个arena。
arena
一种在一个或多个线程之间共享的结构,其中包含对一个或更多heap的引用,以及这些heap中“空闲”的chunk的链表。分配给每个arena的线程将从该arena的空闲列表(bins)中分配内存。

chunk

image.png
使用中的chunk
1、heap中有chunk指针和mem指针 chunk指针指向chunk开始的地址;mem指针指向用户内存块开始的地址。
2、 p=0时,表示前一个chunk为空闲,prev_size才有效
3、p=1时,表示前一个chunk正在使用,prev_size无效 p主要用于内存块的合并操作;ptmalloc 分配的第一个块总是将p设为1, 以防止程序引用到不存在的区域
4、M=1 为mmap映射区域分配;M=0为heap区域分配
5、 A=0 为主分配区分配;A=1 为非主分配区分配。
空闲的chunk
1、当chunk空闲时,其M状态是不存在的,只有AP状态,
2、原本是用户数据区的地方存储了四个指针,
指针fd指向后一个空闲的chunk,而bk指向前一个空闲的chunk,malloc通过这两个指针将大小相近的chunk连成一个双向链表。
在large bin中的空闲chunk,还有两个指针,fd_nextsize和bk_nextsize,用于加快在large bin中查找最近匹配的空闲chunk。不同的chunk链表又是通过bins或者fastbins来组织的。

arenas和heaps

image.png
为了有效地处理多线程应用程序,glibc的malloc允许一次活动多个内存区域。因此,不同的线程可以访问存储器的不同区域,而不会相互干扰。这些记忆区域统称为“arena”。有一个主mrena,即“main arena”,对应于应用程序的初始heap。malloc代码中有一个静态变量指向这个arena,每个arena都有一个下一个指针来链接其他arena。
随着线程碰撞的压力增加,glibc通过mmap创建了额外的arena来缓解压力。arena的数量上限为系统中CPU数量的八倍,这意味着重线程应用程序仍会出现一些争用,但代价是碎片会减少。
每个arena中都有一个mutex,用于控制对该arena的访问。一些操作,例如访问fastbins,可以使用原子操作来完成,并且不需要锁定arena。所有其他操作都要求线程锁定arena。对这个mutex的争用是创建多个arena的原因——分配给不同arena的线程不需要相互等待。如果有争用,线程将自动切换到未使用(未锁定)的arenas。
每个arenas都从一个或多个堆中获得内存。main arenas使用程序的初始堆(从.bss之后开始),可以使用mmap和brk分配内存。其它的arenas只能通过mmap为它们的堆分配内存,每个竞技场都会跟踪一个特殊的“顶部”chunk,这通常是最大的可用chunk,同时指向最近分配的heap。
总结:
1. 主分配区和非主分配区形成一个环形链表进行管理。
2. 每一个分配区利用互斥锁使线程对于该分配区的访问互斥。
3. 每个进程只有一个主分配区,也可以允许有多个非主分配区。
4. ptmalloc根据系统对分配区的争用动态增加分配区的大小
5. 主分配区可以使用brk和mmap来分配,而非主分配区只能使用mmap来映射内存块
6. 申请小内存时会产生很多内存碎片,ptmalloc在整理时也需要对分配区做加锁操作

bins

为了避免频繁的系统调用,应用free的内存块,ptmalloc会根据size和历史存储在不同的bins中。
image.png

fast bins

fast bins是bins的高速缓冲区,大约有10个定长队列。每个fast bin都记录着一条free chunk的单链表(称为binlist ,采用单链表是出于fast bin中链表中部的chunk不会被摘除的特点),增删chunk都发生在链表的前端。
fastbin中的chunks可以根据需要移动到其他bins中。fast bins 记录着大小以8字节递增的bin链表。当用户释放一块不大于max_fast(默认值64B)的chunk的时候,会默认会被放到fast bins上。当需要给用户分配的 chunk 小于或等于 max_fast 时,malloc 首先会到fast bins上寻找是否有合适的chunk,除非特定情况,两个毗连的空闲chunk并不会被合并成一个空闲chunk。不合并可能会导致碎片化问题,但是却可以大大加速释放的过程。

image.png

unsorted bins

unsorted bin 的队列使用 bins 数组的第一个,是bins的一个缓冲区,加快分配的速度。当用户释放的内存大于max_fast或者fast bins合并后的chunk都会首先进入unsorted bin上。unsorted bins无尺寸限制,任何大小chunk都可以添加进这里。unsorted bins的设计主要是为了一个最近释放的复用。
用户malloc时,如果在 fast bins 中没有找到合适的 chunk,则malloc 会先在 unsorted bin 中查找合适的空闲 chunk,如果没有合适的bin,ptmalloc会将unsorted bin上的chunk放入bins上,然后到bins上查找合适的空闲chunk。

small bins

大小小于512字节的chunk被称为small chunk,而保存small chunks的bin被称为small bin。数组从2开始编号,前64个bin为small bins,small bin每个bin之间相差8个字节,同一个small bin中的chunk具有相同大小。每个small bin都包括一个空闲区块的双向循环链表(也称binlist)。
free掉的chunk添加在链表的前端,而所需chunk则从链表后端摘除。两个毗连的空闲chunk会被合并成一个空闲chunk。合并消除了碎片化的影响但是减慢了free的速度。分配时,当samll bin非空后,相应的bin会摘除binlist中最后一个chunk并返回给用户。在free一个chunk的时候,检查其前或其后的chunk是否空闲,若是则合并,也即把它们从所属的链表中摘除并合并成一个新的chunk,新chunk会添加在unsorted bin链表的前端。

large bins

大小大于等于512字节的chunk被称为large chunk,而保存large chunks的bin被称为large bin,位于small bins后面。large bins中的每一个bin分别包含了一个给定范围内的chunk,其中的chunk按大小递减排序,大小相同则按照最近使用时间排列。
两个毗连的空闲chunk会被合并成一个空闲chunk。
分配时,遵循原则“smallest-first , best-fit”,从顶部遍历到底部以找到一个大小最接近用户需求的chunk。一旦找到,相应chunk就会分成两块User chunk(用户请求大小)返回给用户。Remainder chunk(剩余大小添加到unsorted bin。free时和small bin 类似。

三种特殊chunks

有三种特殊chunks不会存储到bins中:

  1. Top chunk
    top chunk相当于分配区的顶部空闲内存,当bins上都不能满足内存分配要求的时候,就会来top chunk上分配。
    当top chunk大小比用户所请求大小还大的时候,top chunk会分为两个部分:User chunk(用户请求大小)和Remainder chunk(剩余大小)。其中Remainder chunk成为新的top chunk。
    当top chunk大小小于用户所请求的大小时,top chunk就通过sbrk(main arena)或mmap(thread arena)系统调用来扩容。
  2. mmaped chunk
    当分配的内存非常大(大于分配阀值,默认128K)的时候,需要被mmap映射,则会放到mmaped chunk上,当释放mmaped chunk上的内存的时候会直接交还给操作系统。
    3、Last remainder chunk
    当在small bins中找不到合适的chunk,如果last remainder chunk的大小大于所需要的small chunk大小,last remainder chunk就会被分裂成两个chunk,其中一个chunk返回给用户,另一个chunk变成新的last remainder chunk。

tccache

线程会在thread local中记住用过的main arenas,如果这个arenas被占用,那么就会阻塞等待其释放。
线程也有自己的cache,被称为_tcache,这个块大小受到限制,_分配时不需要使用arenas,而回退时需要使用arenas。
image.png

内存分配算法

image.png

  1. 如果在tcache中有一个合适的(精确匹配)块,它就会返回给调用者。没有则尝试使用来自较大大小的bins的可用块。
  2. 如果请求足够大,则使用mmap()直接从操作系统请求内存。请注意,mmap的阈值是动态的,可以通过M_mmap_threshold参数修改,并且同时可以有多少mmap是有限制的。
  3. 如果合适的fastbin中有一个chunk,请使用它。如果有其他chunk可用,也可以预填充tcache。
  4. 如果适当的smallbin中有一个chunk,请使用它,可能还会在此处预填充tcache。
  5. 如果请求“很大”,花点时间把fast bins里的所有东西都拿走,然后把它们移到unsorted bins里,边走边合并。
  6. 开始从unsorted bins中取出块,并将它们移到小/大的bins中,边走边合并(注意,这是代码中唯一将块放入小/大bins的地方)。如果看到一个合适大小的chunk,则使用它。
  7. 如果请求是“大”的,则搜索相应的大bin,然后依次搜索更大的bin,直到找到足够大的chunk。
  8. 如果我们在fastbin中仍然有块(这可能发生在“小”请求中),请合并这些块并重复前两个步骤。

内存释放算法

free()调用并不会真正将内存返还到操作系统,而仅标记为可被应用程序重用。如果top chunk内存足够大,那么可能会取消映射。

  1. 如果tcache中有空间,则将块存储在那里并返回。
  2. 如果区块足够小,请将其放入适当的fast bins中。
  3. 如果这个区块是mmap的,就对它进行munmap。
  4. 查看此bins是否与另一个可用bins相邻,如果相邻则合并。
  5. 将区块放在unsorted bins中,除非它现在是top trunk。
  6. 如果thunk足够大,合并所有fastbin,然后如果顶部的thunk是否足够大,将会在os中取消映射。出于性能原因,这一步骤可能会被推迟,并在malloc或其他调用期间发生。

简单聊聊GC场景下的内存释放

结合上面文章,主要是考虑GC一次回收的粒度问题。
以目前jdk默认的G1为例。
按照分代收集理论,新生代会比老年代有更频繁的gc调用。

参考资料

https://ionutbalosin.com/2020/01/hotspot-jvm-performance-tuning-guidelines/
https://man7.org/linux/man-pages/man2/mmap.2.html
https://zhuanlan.zhihu.com/p/166576293
https://www.microsoft.com/en-us/research/uploads/prod/2019/06/mimalloc-tr-v1.pdf
https://zhuanlan.zhihu.com/p/658307892
https://openjdk.org/groups/hotspot/docs/RuntimeOverview.html#Thread%20Management|outline
https://www.oracle.com/technetwork/java/javase/memorymanagement-whitepaper-150215.pdf
https://tldp.org/LDP/lki/lki-4.html
https://keys961.github.io/2019/04/10/Linux%E5%86%85%E6%A0%B8-%E9%A1%B5%E9%AB%98%E9%80%9F%E7%BC%93%E5%AD%98%E4%B8%8E%E9%A1%B5%E5%9B%9E%E5%86%99/
https://www.kernel.org/doc/gorman/html/understand/understand005.html
https://www.infradead.org/~mchehab/rst_conversion/filesystems/vfs.html

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

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

相关文章

leetcode:643. 子数组最大平均数 I(滑动窗口)

一、题目 链接:643. 子数组最大平均数 I - 力扣(LeetCode) 函数原型: double findMaxAverage(int* nums, int numsSize, int k) 二、思路 滑动窗口: 先计算数组前k个元素总和,作为第一个窗口,默…

vlog如何降低重复率

大家好,今天来聊聊vlog如何降低重复率,希望能给大家提供一点参考。 以下是针对论文重复率高的情况,提供一些修改建议和技巧: vlog如何降低重复率 Vlog作为一种流行的视频日志形式,常常被人们用于记录日常生活、分享经…

pta模拟题——7-34 刮刮彩票

“刮刮彩票”是一款网络游戏里面的一个小游戏。如图所示: 每次游戏玩家会拿到一张彩票,上面会有 9 个数字,分别为数字 1 到数字 9,数字各不重复,并以 33 的“九宫格”形式排布在彩票上。 在游戏开始时能看见一个位置上…

Lambda表达式规则,用法

Lambda表达式是JDK8新增的一种语法格式 1.作用 简化匿名内部类的代码写法 Lambad用法前提:只能简化函数式接口(一般加有Funcationallnterface)(有且仅有一个抽象方法)的匿名内部类 匿名内部类:(本质是对…

2023年终总结-轻舟已过万重山

自我介绍 高考大省的读书人 白,陇西布衣,流落楚、汉。-与韩荆州书 我来自孔孟故里山东济宁,也许是小学时的某一天,我第一次接触到了电脑,从此对它产生了强烈的兴趣,高中我有一个愿望:成为一名计…

设计模式再探——装饰模式

目录 一、背景介绍二、思路&方案三、过程1.装饰模式简介2.装饰模式的类图3.装饰模式代码4.装饰模式,职责父类拆分的奥义5.装饰模式,部件抽象类的无中生有 四、总结五、升华 一、背景介绍 最近公司在做架构模型的时候,涉及到装饰模式的研…

html网页设计 01marquee标签广告滚动(1)

<!DOCTYPE html> <html><head><meta charset"utf-8"><title></title></head><body><!-- scrollamount:数字越大&#xff0c;滚动越快direction:滚动方向滚动的类型behaior"slide",文字滚动到边界后就会…

vuepress-----20、全文搜索

默认主题自带的搜索, 只会为页面的标题、h2、h3 以及 tags构建搜索索引。所以尽量将围绕知识点的关键字体现到标题上。而 tags 更为灵活&#xff0c;可以把相关的能想到的关键字都配置到 tags 中&#xff0c;以方便搜索。 默认插件介绍 (opens new window) 默认主体配置 (ope…

电子秤ADC芯片CS1237技术资料问题合集

问题11&#xff1a;实际应用中&#xff0c;多个称重传感器应该怎么与ADC连接&#xff1f; 解答&#xff1a;如果传感器是测量同一物体&#xff08;例如&#xff1a;厨房垃圾处理器&#xff09;&#xff0c;一般建议使用并联的方式。则相同类型的信号线连接在一起。对于传感器的…

C语言指针基础题(一)

目录 例题一题目解析答案 例题二题目解析答案 例题三题目解析答案 例题四题目解析答案 例题五题目解析答案 例题六题目解析答案 例题七题目解析答案 感谢各位大佬对我的支持,如果我的文章对你有用,欢迎点击以下链接 &#x1f412;&#x1f412;&#x1f412; 个人主页 &#x…

C++ 教程 - 01 基础篇

文章目录 C介绍环境配置第一个cpp程序案例练习 变量常量关系运算符逻辑运算符条件运算符位运算符类型转换分支循环程序调用综合案例 C介绍 基于C语言&#xff0c;继承了C的所有语法&#xff1b; 静态类型语言&#xff0c;需要先编译&#xff0c;再执行&#xff1b; 贴近底层硬…

windows下分卷解压文件

我的文件是这样的&#xff1a; 存放路径为&#xff1a;C:\Users\Luli_study\MICCAI_MMAC\fudanuniversity\DDR dataset 首先要进入分卷文件的目录cd&#xff1a; 第一步&#xff1a;cd /path/o/分卷问文件目录 第二步&#xff1a; 执行之后的结果(红色框出来的)&#xff1a; …

Vellum —— Fluid

目录 Vellum fluids setups Fluid tips and troubleshooting Fluid phases Vellum fluids and soft bodies Vellum fluid vs FLIP fluid Vellum fluids setups Vellum fluid solver是基于粒子流体的解算框架&#xff0c;被完全集成到了Vellum动力学系统&#xff08;可与gr…

王道数据结构课后代码题 p149 第3—— 7(c语言代码实现)

目录 3.编写后序遍历二叉树的非递归算法 4.试给出二叉树的自下而上、自右到左的层次遍历算法 &#xff08;有图解代码详解&#xff09;c语言代码实现 5.假设二叉树采用二叉链表存储结构&#xff0c;设计一个非递归算法求二叉树的高度。 ​编辑 6.设一棵二叉树中各结点的值互不…

普冉(PUYA)单片机开发笔记(7): ADC-轮询式多路采样

概述 应用中经常会有使用单片机进行模数转换的需求。PY32F003 具有 1 个 12 位的模拟数字转换器&#xff08;ADC&#xff09;&#xff0c;今天我们一起来使用一下这个 ADC。 数据手册中对 ADC 简介如下。 SAR ADC&#xff1a;逐次逼近式 ADC&#xff0c;原理参见“参考链接&a…

class070 子数组最大累加和问题与扩展-上【算法】

class070 子数组最大累加和问题与扩展-上【算法】 code1 53. 最大子数组和 // 累加和最大子数组和 // 给你一个整数数组 nums // 请你找出一个具有最大累加和的非空子数组 // 返回其最大累加和 // 测试链接 : https://leetcode.cn/problems/maximum-subarray/ dp[i]&#xff…

【Docker】Docker Compose,yml 配置指令参考的详细讲解

作者简介&#xff1a; 辭七七&#xff0c;目前大二&#xff0c;正在学习C/C&#xff0c;Java&#xff0c;Python等 作者主页&#xff1a; 七七的个人主页 文章收录专栏&#xff1a; 七七的闲谈 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01;&#x1f496;&#x1f…

基于c++版数据结构基于数组栈改-Python思维总结

##栈部分-&#xff08;叠猫猫&#xff09; ##抽象数据类型栈的定义&#xff1a;是一种遵循先入后出的逻辑的线性数据结构。 换种方式去理解这种数据结构如果我们在一摞盘子中取到下面的盘子&#xff0c;我们首先要把最上面的盘子依次拿走&#xff0c;才可以继续拿下面的盘子&…

【Java期末复习资料】(2)常见例题 //持续更新

本文章主要是常见例题&#xff0c;解析不会太详细&#xff0c;有问题、不会的可以给我发消息哦&#xff0c;后续会出模拟卷 常见例题&#xff1a; 1.下列跟Java技术平台有关的是&#xff08;ABD&#xff09; A.JVM B.JDK C.JPN D.JRE 2.面向对象的特征包括&#xff08;ACD&…

wxPython的控件tree

wxPython树控件介绍 树&#xff08;tree&#xff09;是一种通过层次结构展示信息的控件&#xff0c;如下图所示是树控件示例&#xff0c;左窗口中是树控件&#xff0c;在wxPython中树控件类是wx.TreeCtrl。 wx.TreeCtrl常用的方法有 AddRoot(text, image-1, selImage-1, data…