【Go万字洗髓经】Golang内存模型与内存分配管理
本文目录
- 1. 操作系统中的虚拟内存
- 分页与进程管理
- 虚拟内存与内存隔离
- 2. Golang中的内存模型
- 内存分配流程
- 内存单元mspan
- 线程缓存mcache
- 中心缓存mcentral
- 全局堆缓存mheap
- heapArena
- 空闲页索引pageAlloc
- 3. Go对象分配
- mallocgc函数
- tiny对象分配内存
- 4.结合GMP模型来看内存模型
- tiny对象分配
- 5.总结
- 设计思想
- 一些问题?
- 为什么mcache与P绑定?
- span的等级到底是66级还是67级或者68级?
- 0级到底是什么?是更大对象吗?
- 6. 参考文章
1. 操作系统中的虚拟内存
虚拟内存是操作系统中一种重要的内存管理技术。它允许计算机系统使用硬盘空间来模拟额外的内存空间,从而扩展可用内存的范围。
从用户程序的角度来看,虚拟内存提供了一个比实际物理内存大得多的地址空间。操作系统通过将程序和数据分段存储在硬盘上的虚拟内存区域,并在需要时动态地将部分数据加载到物理内存中来实现这一功能。
这种方式使得大型程序能够在有限的物理内存环境中运行,同时也提高了内存的利用率。例如,当物理内存不足时,操作系统会将暂时不用的数据或程序代码移动到虚拟内存中,而当这些数据被访问时,再将其调回到物理内存。
虚拟内存的管理涉及到页面置换算法、段页式管理等多种技术,这些技术共同确保了虚拟内存的有效运行,并为用户提供了一个高效且透明的内存使用环境。
比如下面这个图,是一个简单的示意。虚拟内存可以通过页表来定位到真实数据到底位于哪里,从而进行访问数据。
虚拟内存是以“页”进行单位进行管理,物理内存是“帧”。
操作系统中虚拟内存和物理内存被切割成固定尺寸的“页”和“帧”有其特定的意义和好处。首先,这样做可以提高内存空间的利用效率。当内存以页为粒度进行管理时,可以消除不稳定的外部碎片,取而代之的是相对可控的内部碎片。这意味着内存的使用更加高效,减少了浪费。(内部的碎片是指页内的碎片地址,比如说4k,只用了3k,所以多的1k是多余的。)
其次,将内存分割成页和帧可以提高内存与外部存储之间的交换效率。更细的粒度意味着更高的灵活性,操作系统可以更灵活地管理内存,从而提高内外存交换的效率。
此外,这种分割方式与虚拟内存机制相呼应,便于建立虚拟地址到物理地址的映射关系。这种映射关系是通过一种称为页表的数据结构来实现的,它聚合了映射关系,使得虚拟内存的管理和访问更加高效。
在Linux系统中,页或帧的大小是固定的,通常为4KB。这个大小实际上是由实践经验决定的。如果页或帧太大,会增加内存碎片率,导致内存利用不充分;如果太小,则会增加分配频率,影响系统效率。因此,4KB是一个平衡点,既能保证内存的有效利用,又能保持较高的系统效率。
所以总的来说,分页不仅是为了防止外部的内存碎片导致的内存浪费,也还是因为多进程时代内存可能溢出的问题,主要还是为了做进程管理。
另外就是虚拟内存不是只让用户看着空间更大,主要是为了解决内存隔离的问题。
这里简单提一下进程管理和内存隔离,不是本文重点。
分页与进程管理
进程管理是操作系统的一项基本功能,它涉及到进程的创建、调度、执行和终止。进程是操作系统进行资源分配和调度的基本单位。每个进程都有自己的地址空间,操作系统通过进程管理确保每个进程都能安全、有效地使用系统资源,并且与其他进程隔离开来。
内存泄露(Memory Leak)是指程序在申请内存后,未能在不需要时正确释放,导致随着时间的推移,大量内存无法被回收利用,最终可能导致程序或系统性能下降,甚至崩溃。内存泄露是编程中常见的问题,特别是在那些需要频繁动态分配内存的程序中。
分页是现代操作系统中常用的内存管理技术之一。通过将内存分割成固定大小的页(Page),操作系统可以更有效地管理内存,同时也为进程管理提供了便利。每个进程都有自己的一组页,这些页映射到物理内存中。这种隔离机制可以防止一个进程访问或修改另一个进程的内存,从而提高了系统的稳定性和安全性。分页机制不仅有助于防止恶意软件随意访问内存,也有助于防止进程因内存溢出而相互干扰。
虚拟内存与内存隔离
操作系统为每个进程提供了一个独立的虚拟地址空间,进程通过虚拟地址访问内存。操作系统使用页表将虚拟地址映射到物理内存地址,这个过程对进程是透明的。由于每个进程都有其独立的页表,因此它们无法访问其他进程的虚拟内存空间。
分页是实现内存隔离的一种技术,操作系统将内存分割成固定大小的页(Page),每个进程只能访问分配给它的页。如果进程尝试访问未分配给它的页,操作系统会阻止这种访问,从而防止进程之间的内存干扰。
2. Golang中的内存模型
有一个很核心的点是,以空间换时间,一次缓存,多次复用。
因为每次申请内存的代价比较大,所以可以多申请一些内存,方便后续程序不断地使用。如果长时间申请的内存都是闲置的,那么就可以归还给操作系统。
内存分配流程
从操作系统的角度来看,这是用户进程(golang程序)中缓存的内存。
从Go自己的角度来看,堆
是所有对象的内存起源,所有的对象的内存都是“堆”申请到的内存。
为了提高分配内存的效率,Go还设计了多级缓存,从而实现无锁化、细锁化粒度。
我们可以看看下面这个逻辑分层图,注意,是逻辑分层图。只是为了最开始方便理解内存模型,后边随着深入讲解,会不断地延伸。
堆mheap
是全局唯一的,如果要和mheap
进行操作申请内存,需要加一个全局锁。因为堆是全局唯一的,所以这个锁也是全局锁,和进程(Go程序)一对一的。
在mheap
上细化粒度,建立了有mcentral
可以理解为一个等级集合的概念,根据最终需要创建的对象的大小区别,排了一个等级,当想要分配某个内存给某个对象实例的时候,会判断这个实例的大小,然后分配对应的mcentral
。这样就把锁的粒度细化到了mcentral
。也就是同一个大小等级内的所有对象,去竞争这个锁,优化了性能。
mcache
就是GMP调度器中的处理器Process
,mcache
就是每一个处理器P独一份的、本地私有的缓存mcache
,mcache
中会冗余每一种等级的空间mspan
,也就是会为每一个处理器去冗余一个内存空间。
当去获取内存的时候,先根据这个Process
去查看其本地私有的mcache
中有没有适合的内存空间使用,如果有,就直接获取使用,因为是
中私有的,所以不涉及并发,是无锁的,这是最理想的情况下。
如果说mcache
没有空间,没有办法通过无锁的形式进行获取内存的行为,就会把这个行为升级,去mcentral
中想办法分配内存。如果还是不行,就会继续升级,就回去mheap
中获取内存空间。如果还是不行,就会发起系统调用的指令,去虚拟内存中申请更多的空间给mheap
,然后再给我们的这次行为分配内存使用。
所以大概可以梳理个流程了。假设我们正在运行一个Go协程,该程序需要创建一个大小为128字节的对象。以下是分配这个对象可能经历的步骤:
- 本地缓存查找
(mcache)
程序首先检查它所属的处理器P的本地私有缓存mcache
中是否有足够大的内存空间mspan
来存放这个128字节的对象。mcache
中为每种大小的对象都准备了冗余的内存空间,以减少锁的竞争和提高效率。
如果mcache
中有合适大小的mspan
,那么程序将直接从mcache
中分配内存,这个过程是无锁的,因为每个处理器都有自己的mcache
,不涉及并发问题。
- 中央缓存查找
(mcentral)
如果mcache
中没有合适大小的mspan
,程序将尝试从mcentral
中获取。mcentral
是一个按对象大小分类的中央缓存,它管理着相同大小对象的内存分配。
在mcentral
中,程序会找到管理128字节对象的mcentral
实例。由于mcentral
是按大小分类的,所以这里的锁竞争仅限于相同大小的对象,这进一步细化了锁的粒度,提高了性能。
如果mcentral
中有空闲的mspan
,它将被分配给程序。如果没有,mcentral
将尝试从mheap
中获取新的内存空间。
- 堆内存分配
(mheap)
如果mcentral
也无法提供内存,那么程序将直接向mheap
申请内存。mheap
是Go程序的全局堆内存,负责管理整个程序的内存分配。
由于mheap是全局唯一的
,操作mheap
需要加一个全局锁,以确保内存分配的原子性和一致性。这是整个内存分配过程中锁粒度最大的一步,但因为前面的步骤已经尽可能地减少了对mheap
的直接操作,所以这种情况相对较少。
- 系统调用(如果必要)
如果mheap
也没有足够的内存,那么程序将通过系统调用向操作系统请求更多的虚拟内存空间,然后将这部分空间添加到mheap
中,再进行内存分配。
内存单元mspan
mcentral
会以我们为某个实例对象所需要分配的内存的大小来建立不同的等级,那么这个大小等级是怎么划分的?
Golang中有两个概念,最小的存储单元-8KB:Page
和最小的管理单元:mspan
。
最小的存储单元也称为页,page
,大小为8KB
。
mspan
里边的obj大小,从8B
到32KB
( 32,768 字节 B
)被划分67种不同的规格【2025年3月10日,go源码最新确认是67种,大部分教程可能是两年前时候的,两年前是66种,https://github.com/golang/go/blob/master/src/runtime/sizeclasses.go ,Go仓库链接。】,分配对象的内存的时候,会根据大小映射到不同规格的mspan
。(所以下面的图划错了,最高应该是32KB)
mcentral
会根据不同mspan
的等级,有不同的central
的实例,每个实例会以一个双向链表的形式来管理mspan
。
所以mspan的特性是如图下所示的。双向链表、起始page、page的页数等,用来连续标识。
前面我们提到,mspan
都是page
的整数倍,page
是8KB大小的,当mspan
的obj等级为8B时,那么mspan
里边就需要划分很多内存块object
。mspan
内部的页是连续的,至少在虚拟内存的视角中是这样,因为虚拟内存分配了连续的空间给go。
同等级的mspan
会从属同一个mcentral
,一个mcentral
会把这些同等级的span
构造成链表,所以上边是双向链表,有两个指针。并且使用一个锁进行互斥,来管理。
mspan
会基于位图算法bitMap
来快速找到对应的空闲块object
,块大小对应等级的大小。使用的是ctz64
算法。
也就是下面这个分配示意图,为0代表被占用,为1代表free可以分配出去。
mspan
等级被划分为1-67,67级,此外还有个0级,用于处理特殊对象。
class1,就是8bytes,即8B,也就是一个8B的对象,mspan为8KB时,就代表这个mspan可以分配1024个对象出去。
当分配的对象为0-8B,都会使用calss1
对应的span
。而不是只有8B刚刚好时才进行分配。这也会导致有内存浪费。
这也会导致一个tail waste,末尾浪费,当class为3时,obj对象大小为24B,那么8KB=8192B的span会不能完全分配完obj,会造成末尾浪费,也就是8B,341x24+8=8192B。
除此之外还有max waste
,代表了这个mspan
分配的时候最多可能会造成的空间浪费。这个也很好理解,当所有对象为17B的时候,分配了341个出去,那么一共会造成total = (24-17)x341 + 8
空间的浪费,这个总共的空间除以 total / 8192 = 29%
,这就是class
为3时的 max waste
为 29%。
每个object还有个一个很重要的属性是nocan,也就是是否object包含了指针,在垃圾回收gc时是否需要展开对应的标记。
在go中,span class
和 nocan
两个部分信息会组成一个 uint8
,形成完整的spanclass
标识,8个bit中,高7位标识了span
中一共66个等级,最低位标识nocan
就可以了。
线程缓存mcache
mcache
是每个P独有的缓存,因此交互无锁。mcache
将每种spanClass
等级的mspan
都各自缓存了一个,同时分为scan
和nocan
两个系列,也就是是否在gc时需要展开。一共是68*2=136
个。
mcache
还有一个tiny allocator
微对象分配器,用于处理小于16B的对象内存分配。(参考了TCMalloc
。)
中心缓存mcentral
每个central
会对应一种等级的spanClass
,然后把spanclass分为两类,分别是有空间的mspan
链表partial还有满空间mspan
链表full。
每个central
会有一把锁,这就是细化锁的粒度。可以把mcentral
看成是mheap
的一部分,只不过会优先从 MCentral
获取内存,如果没有 MCentral 会从 Arenas
中的某个 HeapArena
获取 Page。
全局堆缓存mheap
从go上层应用的角度来看,堆就是操作系统虚拟内存的抽象,可以看作是代言人。
mheap
以页为单位,8KB大小,作为最小内存存储单元。注意与之前讲过的span
的内存管理单元区分。
基于bitMap
标识每个页的使用情况,每个bit对应一页,为0就是代表可以用,为1的话代表已经被mspan给分配走,但是不一定已经被obj对象使用了。
mheap
有一个聚合页heapArena
,有记录页到其所从属的mspan
的映射信息。这是为了方便在gc时进行操作。
建立空闲页基数树索引radix tree index
,帮助我们能够快速找到空闲页。因为我们刚刚说过,mspan
中需要的page
是连续的,所以如何通过bitMap来快速找到 连续+空闲 的页page
,是需要考虑的,也就是这个的目的,能够找到符合我们需求数量的空闲页。
mheap
是mcentral
的持有者,持有所有spanClass下的mcentral
,作为自身的缓存。可以把mcentral看成mheap更细化粒度的缓存。那么,我们应该如何理解这句话?
首先,mcentral
可以看成是一次性从mheap
中分配一系列的空间去给上层使用。
也就是说,mheap
是mcentral
的持有者,这意味着mheap
负责管理所有的mcentral
实例。每个mcentral
实例对应一个特定的spanClass
,用于缓存特定大小的内存块。
mcentral
作为mheap
的缓存,这意味着mheap
通过mcentral
来间接管理内存块。当需要分配内存时,首先会检查相应的mcentral
是否有可用的内存块。如果有,就直接从mcentral
中分配;如果没有,mheap
会负责从操作系统申请新的内存,然后将其添加到相应的mcentral
中。
当内存不够时,mheap
会向操作系统申请,申请单位为heapArena
,64M
。
heapArena
通过下面这个图来快速知道heapArena
的概念。(下面图中的单词拼多了一个a,应该为heapArena)
我们说过,mheap上游是mcentral,mcentral中的mspan如果不够了会向mheap申请,mheap下游就是直接跟操作系统虚拟内存对接,mheap如果还不够,就直接向虚拟内存申请了,一次性申请的大小是heapArena,也就是64MB,访问mheap的时候需要加锁,因为是全局唯一的。
mheap
是对内存块的管理对象,通过page
为最小内存存储单元进行管理。一系列的page
组合成一个heapAreana
。
所以,每个 heapArena
包含 8192 个页,大小为 8192 * 8KB = 64 MB。
heapArena
记录了页到 mspan
的映射. 因为 GC 时,通过地址偏移找到页很方便,但找到其所属的 mspan
不容易,所以我们需要通过这个映射信息进行辅助。
每个heapArena包含一个bitmap,标记当前这个heapArena的使用情况。主要是为了GC垃圾回收,bitmap有两种标记,一种是标记对应地址中是否存在对象,另一种标记这个对象是否被GC模块标记过,所以当前heapArena中所有的Page都会被bitmap标记。
空闲页索引pageAlloc
到这里已经有些比较绕了,再回顾一下这个图。注意这个只是逻辑图!
首先,pageAlloc
是一种基于基数树(Radix Tree)
的索引结构,它用于快速查找和分配空闲页。pageAlloc
通过组织和管理空闲页的索引信息,优化了内存分配过程中的查找效率,从而提高了内存分配的性能。基数树是一种高效的数据结构,它能够快速地定位和检索数据,这使得pageAlloc
能够迅速找到满足分配要求的连续页空间。在内存分配过程中,pageAlloc
会根据需要分配的页数量,在基数树中查找合适的空闲页范围,如果找到合适的空闲页,就进行分配;如果没有找到,则可能需要触发垃圾回收或者向操作系统请求更多的内存资源。
3. Go对象分配
Go中分配对象的方式有几种常见的,比如new(T)
、&T{}
、make(T)
等。这几种方法都会最终通过mallocgc
方法进行分配。
Go会根据obj的大小,将对象分为3类,分别是tiny微对象(0,16B)
、small小对象【16B,32KB】
、large大对象(32KB以上)
。
不同类型的对象,会有不同的分配策略,这些分配策略可以在mallocgc
方法中查看。
微对象的分配流程如下。
- 从P专属的
mcache
的tiny分配器中取对应内存,这个过程是无锁的。 - 根据对应的
spanClass
,从p专属mcache缓存
的mspan
中取内存,无锁。 - 根据对应的
spanClass
从对应的mcentral
中取msapn
填充到mcache
,然后从mspan
中取内存,spanClass
粒度的锁。 - 根据对应的
spanClass
,从mheap
的页分配器pageAlloc
中取得足够数量空闲页组装成mspan
填充到mcentral
中,然后再填充到mcache
中,然后从mspan
中取内存,涉及到了mheap
,所以是全局锁。 mheap
向操作系统申请内存,更新页分配器的索引信息,然后重复步骤4.
小对象的分配流程就是跳过上述的步骤1,直接执行2-5即可。
对于大的对象,跳过步骤1-3,直接执行步骤4和5,因为大对象是0号等级,所以在mcentral
里面找不到对应的spanClass
等级,只能去从步骤4开始直接与堆进行交互操作。
mallocgc函数
进行对象实例的时候,都会进行mallocgc
这个方法。
malloc
是内存分配的意思,gc是垃圾回收的意思,这个函数不仅是进行内存分配,还是gc垃圾回收的入口,所以叫做mallocgc
。
malloc.go
代码如下。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {// ... // 获取 mmp := acquirem()// 获取当前 p 对应的 mcachec := getMCache(mp)var span *mspanvar x unsafe.Pointer// 根据当前对象是否包含指针,标识 gc 时是否需要展开扫描noscan := typ == nil || typ.ptrdata == 0// 是否是小于 32KB 的微、小对象if size <= maxSmallSize {// 小于 16 B 且无指针,则视为微对象if noscan && size < maxTinySize {// tiny 内存块中,从 offset 往后有空闲位置off := c.tinyoffset// 如果大小为 5 ~ 8 B,size 会被调整为 8 B,此时 8 & 7 == 0,会走进此分支if size&7 == 0 {// 将 offset 补齐到 8 B 倍数的位置off = alignUp(off, 8)// 如果大小为 3 ~ 4 B,size 会被调整为 4 B,此时 4 & 3 == 0,会走进此分支 } else if size&3 == 0 {// 将 offset 补齐到 4 B 倍数的位置off = alignUp(off, 4)// 如果大小为 1 ~ 2 B,size 会被调整为 2 B,此时 2 & 1 == 0,会走进此分支 } else if size&1 == 0 {// 将 offset 补齐到 2 B 倍数的位置off = alignUp(off, 2)}
// 如果当前 tiny 内存块空间还够用,则直接分配并返回if off+size <= maxTinySize && c.tiny != 0 {// 分配空间x = unsafe.Pointer(c.tiny + off)c.tinyoffset = off + sizec.tinyAllocs++mp.mallocing = 0releasem(mp) return x} // 分配一个新的 tiny 内存块span = c.alloc[tinySpanClass] // 从 mCache 中获取v := nextFreeFast(span) if v == 0 {// 从 mCache 中获取失败,则从 mCentral 或者 mHeap 中获取进行兜底v, span, shouldhelpgc = c.nextFree(tinySpanClass)}
// 分配空间 x = unsafe.Pointer(v)(*[2]uint64)(x)[0] = 0(*[2]uint64)(x)[1] = 0size = maxTinySize} else {// 根据对象大小,映射到其所属的 span 的等级(0~66)var sizeclass uint8if size <= smallSizeMax-8 {sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]} else {sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]} // 对应 span 等级下,分配给每个对象的空间大小(0~32KB)size = uintptr(class_to_size[sizeclass])// 创建 spanClass 标识,其中前 7 位对应为 span 的等级(0~66),最后标识表示了这个对象 gc 时是否需要扫描spc := makeSpanClass(sizeclass, noscan) // 获取 mcache 中的 spanspan = c.alloc[spc] // 从 mcache 的 span 中尝试获取空间 v := nextFreeFast(span)if v == 0 {// mcache 分配空间失败,则通过 mcentral、mheap 兜底 v, span, shouldhelpgc = c.nextFree(spc)} // 分配空间 x = unsafe.Pointer(v)// ...} // 大于 32KB 的大对象 } else {// 从 mheap 中获取 0 号 spanspan = c.allocLarge(size, noscan)span.freeindex = 1span.allocCount = 1size = span.elemsize // 分配空间 x = unsafe.Pointer(span.base())} // ...return x
}
tiny对象分配内存
P独有的mcache会有一个微对象分配器,基于offset偏移线性移动的方式对微对象进行分配,每16B是一个块,对象依据其大小,向上取整为2的整数次幂(2、4、8、16)进行空间补齐,然后进行分配。
如果tiny
对象分配器没有分配成功,那么就会到mcache
分配。
首先根据对象的大小,映射给其所属的mspan
的等级。对应span等级下,分配给每个对象的空间大小,尝试获取mcache
中的span
,如果分配失败,就通过mcentral、mheap
继续。
// 根据对象大小,映射到其所属的 span 的等级var sizeclass uint8// get size class .... // 对应 span 等级下,分配给每个对象的空间大小(0~32KB)// 包含了noscan,组装在一起得到spanClassspc := makeSpanClass(sizeclass, noscan) // 获取 mcache 中的 spanspan = c.alloc[spc] // 从 mcache 的 span 中尝试获取空间 // 通过ctz64算法,在bit map上找到首个obj空位// 也就是在mspan中,用ctz64算法,根据mspan.allocCache的bitmap信息快速找到空闲的object块并且返回。v := nextFreeFast(span)if v == 0 {// mcache 分配空间失败,则通过 mcentral、mheap 继续 v, span, shouldhelpgc = c.nextFree(spc)} // 分配空间 x = unsafe.Pointer(v)
当mspan
也没有可以分配的obj内存块的时候,会进入到mcache.nextFree
方法进行继续获取空间的操作。
也就是上面代码中的。
if v == 0 {// mcache 分配空间失败,则通过 mcentral、mheap 继续 v, span, shouldhelpgc = c.nextFree(spc)}
从mcentral
或者mheap
中获取到了新的span
之后,填充到mcache
的alloc
中的span
集合当中去,然后再把对应的方法返回。
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {s = c.alloc[spc]// ...// 从 mcache 的 span 中获取 object 空位的偏移量freeIndex := s.nextFreeIndex()if freeIndex == s.nelems {// ...// 倘若 mcache 中 span 已经没有空位,则调用 refill 方法从 mcentral 或者 mheap 中获取新的 span c.refill(spc)// ...// 再次从替换后的 span 中获取 object 空位的偏移量s = c.alloc[spc]freeIndex = s.nextFreeIndex()}// ...v = gclinkptr(freeIndex*s.elemsize + s.base())s.allocCount++// ...return
}
4.结合GMP模型来看内存模型
已经完整的对整个内存模型有了解了,接下来可以结合下GMP来看看内存模型,帮助我们更好的梳理。
再来回顾一下关键的一些概念,Page
是Go中内存管理与虚拟内存交互内存的最小单元,8KB大小。mspan
就是一组连续的Page
,mspan
的大小是page
的整数倍。
mcache
是与GMP模型中的P所绑定,而不是线程绑定,真正可运行的线程M的数量与P的数量一致,也就是GOMAXPROCS
个。mcache
与P绑定可以更节省内存空间的使用,保证每个G使用mcache
的时候不需要加锁就可以获得内存。
实际上我们上层应用向go内存模型取内存,就是从span中分配一个obj出去。在上边我们已经提到过一次了。
span size class
是一块内存的所属规模大小,是针对obj size
来计划分的,比如obj
在1-8B
之间的都属于 size class 1
级别,obj大小在8B-16B
之间的都数据size Class 2
级别。
span size class
是 针对span
进行划分的,是span
大小的级别,一个span size class
会对应两个span ,其中一个span存放需要GC扫描的对象,也就是包含了指针的对象,另一个span包含不需要GC的对象。
我们提到过mcache
会冗余136个spanClass
,也就是68x2
,分别对应scan和noscan。
所以mcache
的展开内部结构就是这样对应的关系。协程从mcache上获取内存不需要加锁,因为一个P只有一个M(线程)在上面运行,不可能出现竞争,所以没有锁的限制,加速了内存的分配。
mcache
中每个span class
都会对应一个mspan
,不同的span class
的mspan
的总大小不一样,所以需要的page
也不一样。如图所示,比较清晰能够看出其中关系。
go对内存规格为0的对象(也就是span class 为0 和1)申请做了特殊处理,也就是更大的内存或者真正的0内存对象,直接会返回一个固定地址,也就是直接跟mheap交互获得地址,而不会走正常的内存管理逻辑。
如果申请struct{}、[0]int
,这种,就会直接返回一个固定地址。
这也是为什么通过channel做同步的时候,发送一个struct{}数据,不会申请任何内存,能够节省内存空间。
协程与mcache
的内存交换单位是obj,mcache
与mcentral
的内存交换单位是span
。
mcentral
对于每个级别会存两个span list
链表,一个是没有空间的span list
,一个是空的span list。
表示还有可用空间的 Span
链表。链表中的所有 Span 都至少有 1 个空闲的 Object 空间。如果 mcentral
上游 MCache
退还 Span
,会将退还的 Span
加入到 NonEmpty Span List
链表中。
tiny对象分配
int32、byte、bool这种tiny微对象如过没有tiny分配的情况下,会经常申请一个8B的空间,这样类似bool或者1个字节的byte,也都会独享这个8B的空间,会造成空间浪费。
如果协程申请的空间小于等于8B,那么会匹配的span size class = 1
的8B空间。
而Tiny空间是从span size class =2
中获取一个16B的obj作为tiny的对象的分配空间。
当大量的微小对象都是用8B的时候会造成大量浪费,所以将小于16B的申请统一归为tiny微对象申请。然后以字节对齐的方式进行内存分配。
需要注意的是,如果申请的对象有指针,会进入小对象的申请流程(因为需要GC扫描流程),而没有指针,才会进入tiny微对象申请流程,如果tiny空间的16B没有多余的内存大小了,会从span size class = 2(也就是第一个noscan的mspan中)申请一个16B的object对象放在tiny空间中。
5.总结
设计思想
下次有机会再梳理一篇TCMalloc的文章。
无论是操作系统虚拟内存管理,还是 C++ 的 TCMalloc、Golang 内存模型,均有一个共同特点,就是分层的缓存机制。
针对不同的内存场景采用不同的独特解决方式,提高局部性逻辑和细微粒度内存的复用率。这也是程序设计的至高理念。
一些问题?
为什么mcache与P绑定?
这里查阅了一些资料包括GPT,按我的理解应该如下:
首先可以, 减少锁竞争:由于每个 P 都有自己的 MCache,当多个 goroutine 在不同的 P 上执行时,它们各自的 MCache 是隔离的,不需要加锁就能获得内存分配。
另外是,避免内存浪费:如果 MCache 直接与 M 绑定,那么每个线程的内存缓存会相对独立且会有较高的内存占用。并且最关键的是,真正可运行的线程M的数量与P的数量一致,如果mcache与线程绑定,那么很多线程是会空闲的,而不是真正可运行的。所以M可运行的数量因为=P的数量,那么与 P 绑定的话就可以通过合理共享内存缓存来节省内存空间。
span的等级到底是66级还是67级或者68级?
截止目前,3月5日,Go官方github中的代码注释是1-67种,算上0,一共是68种,可以看到源代码的相关参考如下。
[https://github.com/golang/go/blob/master/src/runtime/sizeclasses.go
]官方地址如上,很多博客或者资料写的是1-66种,可能是因为两年前的版本,是1-66种,目前已经是67种了。
//go:generate go run mksizeclasses.gopackage runtime// class bytes/obj bytes/span objects tail waste max waste min align
// 1 8 8192 1024 0 87.50% 8
// 2 16 8192 512 0 43.75% 16
// 3 24 8192 341 8 29.24% 8
// 4 32 8192 256 0 21.88% 32
// 5 48 8192 170 32 31.52% 16
// 6 64 8192 128 0 23.44% 64
// 7 80 8192 102 32 19.07% 16
// 8 96 8192 85 32 15.95% 32
// 9 112 8192 73 16 13.56% 16
// 10 128 8192 64 0 11.72% 128
// 11 144 8192 56 128 11.82% 16
// 12 160 8192 51 32 9.73% 32
// 13 176 8192 46 96 9.59% 16
// 14 192 8192 42 128 9.25% 64
// 15 208 8192 39 80 8.12% 16
// 16 224 8192 36 128 8.15% 32
// 17 240 8192 34 32 6.62% 16
// 18 256 8192 32 0 5.86% 256
// 19 288 8192 28 128 12.16% 32
// 20 320 8192 25 192 11.80% 64
// 21 352 8192 23 96 9.88% 32
// 22 384 8192 21 128 9.51% 128
// 23 416 8192 19 288 10.71% 32
// 24 448 8192 18 128 8.37% 64
// 25 480 8192 17 32 6.82% 32
// 26 512 8192 16 0 6.05% 512
// 27 576 8192 14 128 12.33% 64
// 28 640 8192 12 512 15.48% 128
// 29 704 8192 11 448 13.93% 64
// 30 768 8192 10 512 13.94% 256
// 31 896 8192 9 128 15.52% 128
// 32 1024 8192 8 0 12.40% 1024
// 33 1152 8192 7 128 12.41% 128
// 34 1280 8192 6 512 15.55% 256
// 35 1408 16384 11 896 14.00% 128
// 36 1536 8192 5 512 14.00% 512
// 37 1792 16384 9 256 15.57% 256
// 38 2048 8192 4 0 12.45% 2048
// 39 2304 16384 7 256 12.46% 256
// 40 2688 8192 3 128 15.59% 128
// 41 3072 24576 8 0 12.47% 1024
// 42 3200 16384 5 384 6.22% 128
// 43 3456 24576 7 384 8.83% 128
// 44 4096 8192 2 0 15.60% 4096
// 45 4864 24576 5 256 16.65% 256
// 46 5376 16384 3 256 10.92% 256
// 47 6144 24576 4 0 12.48% 2048
// 48 6528 32768 5 128 6.23% 128
// 49 6784 40960 6 256 4.36% 128
// 50 6912 49152 7 768 3.37% 256
// 51 8192 8192 1 0 15.61% 8192
// 52 9472 57344 6 512 14.28% 256
// 53 9728 49152 5 512 3.64% 512
// 54 10240 40960 4 0 4.99% 2048
// 55 10880 32768 3 128 6.24% 128
// 56 12288 24576 2 0 11.45% 4096
// 57 13568 40960 3 256 9.99% 256
// 58 14336 57344 4 0 5.35% 2048
// 59 16384 16384 1 0 12.49% 8192
// 60 18432 73728 4 0 11.11% 2048
// 61 19072 57344 3 128 3.57% 128
// 62 20480 40960 2 0 6.87% 4096
// 63 21760 65536 3 256 6.25% 256
// 64 24576 24576 1 0 11.45% 8192
// 65 27264 81920 3 128 10.00% 128
// 66 28672 57344 2 0 4.91% 4096
// 67 32768 32768 1 0 12.50% 8192
0级到底是什么?是更大对象吗?
小徐先生1212教程中写道0级是为了更大对象的申请,可是更大对象的申请应该是直接跟mheap进行申请,并不是所谓的0级,在刘丹冰老师的博客中,验证了申请0级span class对象的时候,返回的地址都是一样的。所以我觉得0级应该是特殊对象,比如struct{}这种,用来做channel通道通信。
我们来看看malloc.go这部分的源码。可以很清楚的看到,当大于32KB的时候,直接从heap中申请。
而当size==0
的时候,直接返回一个zerobase
的地址,那么这个zerobase
是什么呢?
// Al Allocate an object of size bytes.
// Sm Small objects are allocated from the per-P cache's free lists.
// La Large objects (> 32 kB) are allocated straight from the heap.
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// ……(省略部分代码)if size == 0 {
return unsafe.Pointer(&zerobase)
}//……(省略部分代码)
}
运行下面的测试代码,看看输出结果。
//第一篇/chapter3/MyGolang/zeroBase.go
package mainimport (
"fmt"
)func main() {
var (
//0内存对象
a struct{}
b [0]int//100个0内存struct{}
c [100]struct{}//100个0内存struct{},make申请形式
d = make([]struct{}, 100)
)fmt.Printf("%p\n", &a)
fmt.Printf("%p\n", &b)
fmt.Printf("%p\n", &c[50]) //取任意元素
fmt.Printf("%p\n", &(d[50])) //取任意元素
}
运行结果如下,可以看到全部的 0 内存对象分配,返回的都是一个固定的地址。
go run zeroBase.go
0x11aac78
0x11aac78
0x11aac78
0x11aac78
6. 参考文章
本文撰写过程中主要有参考以下两位老师的文章教程,感谢:
刘丹冰老师的Go三关:https://learnku.com/articles/68142
小徐先生1212的教程:https://www.bilibili.com/video/BV1bv411c7bp
相关文章:
【Go万字洗髓经】Golang内存模型与内存分配管理
本文目录 1. 操作系统中的虚拟内存分页与进程管理虚拟内存与内存隔离 2. Golang中的内存模型内存分配流程内存单元mspan线程缓存mcache中心缓存mcentral全局堆缓存mheapheapArena空闲页索引pageAlloc 3. Go对象分配mallocgc函数tiny对象分配内存 4.结合GMP模型来看内存模型tiny…...
mov格式视频如何转换mp4?
mov格式视频如何转换mp4?在日常的视频处理中,经常需要将MOV格式的视频转换为MP4格式,以兼容更多的播放设备和平台。下面给大家分享如何将MOV视频转换为MP4,4款视频格式转换工具分享。 一、牛学长转码大师 牛学长转码大师是一款功…...
鸿蒙OS开发ForEach循环渲染
摘要 在ForEach循环渲染过程中,如果修改列表项中的数据,但是UI页面不会刷新。在最近开发公司app时遇到了这个问题,经过查看官方文档找到了解决方式 官方地址:数据变化不刷新 一、具体解决方案 思路:通过父子组件传…...
【算法】DFS、BFS、拓扑排序
⭐️个人主页:小羊 ⭐️所属专栏:算法 很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~ 目录 持续更新中...1、DFS2、BFSN 叉树的层序遍历二叉树的锯齿形层序遍历二叉树最大宽度 3、多源BFS腐烂的苹果 4、拓扑排序 持续更新中…...
【Godot4.0】贝塞尔曲线在游戏中的实际应用
概述 之前研究贝塞尔曲线绘制,完全是以绘图函数,以及实现节点连接为思考。并没有实际考虑贝塞尔曲线在游戏中的应用。今日偶然看到悦千简一年多前发的一个用贝塞尔曲线实现追踪弹或箭矢效果,还有玩物不丧志的老李杀戮尖塔系列中的卡牌动态箭…...
MongoDB 数据导出与导入实战指南(附完整命令)
1. 场景说明 在 MongoDB 运维中,数据备份与恢复是核心操作。本文使用 mongodump 和 mongorestore 工具,演示如何通过命令行导出和导入数据,解决副本集连接、路径指定等关键问题。 2. 数据导出(mongodump) 2.1 导出命…...
『Rust』Rust运行环境搭建
文章目录 rust编译工具rustupVisual Studio VS Code测试编译手动编译VSCode编译配置 参考完 rust编译工具rustup https://www.rust-lang.org/zh-CN/tools/install 换源 RUSTUP_DIST_SERVER https://rsproxy.cn RUSTUP_UPDATE_ROOT https://rsproxy.cn修改rustup和cargo的安…...
CPU+GPU结合的主板设计思路与应用探讨
在高性能计算和图形处理需求不断增长的背景下,CPUGPU结合的主板设计逐渐成为硬件架构的重要趋势。本文将探讨基于CPUGPU架构的主板设计思路、关键技术考量以及应用前景。 1. 设计思路概述 CPU(中央处理器)擅长处理复杂的逻辑运算和多任务控制…...
latex问题汇总
latex问题汇总 环境问题1 环境 texlive2024 TeXstudio 4.8.6 (git 4.8.6) 问题1 编译过程有如下错 ! Misplaced alignment tab character &. l.173 International Conference on Infrared &Millimeter Waves, 2004: 667--... I cant figure out why you would wa…...
学习springboot-Bean管理(Bean 注册,Bean 扫描)
Bean 扫描 可以浏览下面的博客链接 :spring 学习 (注解)-CSDN博客 在学习spring 注解时,我们使用 Component ,Service,Controller等 这样的注解,将目标类信息,传递给IOC容器,为其创…...
iOS开发,SQLite.swift, Missing argument label ‘value:‘ in call问题
Xcode16中,集成使用SQLite.swift,创建表的时候: let id Expression<Int64>("id"),报错Missing argument label value: in call 直接使用SQLite.Expression<Int64>("id") 或者定义一个全局typ…...
【GIT】重新初始化远程仓库
有的时候我们克隆远端仓库会出错: git clone --depth 1 git116.*.*.*:/srv/customs.git D:\dev\projects\kdy\customs11\customs Cloning into D:\dev\projects\kdy\customs11\customs... remote: Enumerating objects: 1494, done. remote: Counting objects: 100…...
Vue3中 ref 与 reactive区别
ref 用途: ref 通常用于创建一个响应式的基本类型数据(如 string、number、boolean 等),但它也可以用于对象或数组 返回值: ref 返回一个带有 .value 属性的对象,访问或修改数据需要通过 .value 进行 使用场景: …...
apollo3录音到wav播放解决方法
SDK DEMO项目:ap3bp_evb_vos_pcm_recorder_20210901 pcm_recorder.c //***************************************************************************** // // Options // //***************************************************************************** #define PRINT…...
信号处理抽取多项滤波的数学推导与仿真
昨天的《信号处理之插值、抽取与多项滤波》,已经介绍了插值抽取的多项滤率,今天详细介绍多项滤波的数学推导,并附上实战仿真代码。 一、数学变换推导 1. 多相分解的核心思想 将FIR滤波器的系数 h ( n ) h(n) h(n)按相位分组,每…...
Java网络多线程
网络相关概念: 关于访问: IP端口 因为一个主机上可能有多个服务, 一个服务监听一个端口,当你访问的时候主机通过端口号就能知道要和哪个端口发生通讯.因此一个主机上不能有两个及以上的服务监听同一个端口. 协议简单来说就是数据的组织形式 好像是两个人交流一样,要保证自己说…...
linux centos 忘记root密码拯救
在CentOS 7中,如果忘记root密码,可以通过修改系统启动参数进入单用户模式或紧急模式进行重置。以下是两种常用方法,适用于物理机或虚拟机环境: 方法一:通过rd.break参数重置密码 步骤: 重启系统并进入GRU…...
C# 事件使用详解
总目录 前言 在C#中,事件(Events)是一种基于委托的重要机制,用于实现对象之间的松耦合通信。它通过发布-订阅模式(Publisher-Subscriber Pattern),允许一个对象(发布者)…...
flink cdc同步mysql数据
一、api 添加依赖 <dependency><groupId>org.apache.flink</groupId><artifactId>flink-connector-mysql-cdc</artifactId><!-- 请使用已发布的版本依赖,snapshot 版本的依赖需要本地自行编译。 --><version>3.3-SNAP…...
tomcat负载均衡配置
这里拿Nginx和之前做的Tomcat 多实例来实现tomcat负载均衡 1.准备多实例与nginx tomcat单机多实例部署-CSDN博客 2.配置nginx做负载均衡 upstream tomcat{ server 192.168.60.11:8081; server 192.168.60.11:8082; server 192.168.60.11:8083; } ser…...
Ceph(1):分布式存储技术简介
1 分布式存储技术简介 1.1 分布式存储系统的特性 (1)可扩展 分布式存储系统可以扩展到几百台甚至几千台的集群规模,而且随着集群规模的增长,系统整体性能表现为线性增长。分布式存储的水平扩展有以下几个特性: 节点…...
16、JavaEE核心技术-EL与 JSTL
EL与 JSTL 实践 一. EL(Expression Language) EL(表达式语言)是 JSP 2.0 中引入的一种简单的脚本语言,用于在 JSP 页面中简化数据的访问和显示。它通过一种类似于 JavaScript 的语法,允许开发者在 JSP 页面…...
RabbitMQ报错:Shutdown Signal channel error; protocol method
报错信息: Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code406, reply-textPRECONDITION_FAILED - unknown delivery tag 1, class-id60, method-id80) 原因 默认情况下 RabbitMQ 是自动ACK(确认签收&…...
使用DeepSeek完成一个简单嵌入式开发
开启DeepSeek对话 请帮我使用Altium Designer设计原理图、PCB,使用keil完成代码编写;要求:使用stm32F103RCT6为主控芯片,控制3个流水灯的原理图 这里需要注意,每次DeepSeek的回答都不太一样。 DeepSeek回答 以下是使…...
NLP技术介绍
NLP技术介绍 语言分析技术分词词性标注命令实体识别句法分析语义分析文本处理技术文本分类文本聚类情感分析文本生成机器翻译对话系统与交互技术聊天机器人问答系统语音识别与合成知识图谱与语义理解技术知识图谱语义搜索语义推理深度学习与预训练模型循环神经网络(RNN)及其变…...
pycharm + anaconda + yolo11(ultralytics) 的视频流实时检测,保存推流简单实现
目录 背景pycharm安装配置代码实现创建本地视频配置 和 推流配置视频帧的处理和检测框绘制主要流程遇到的一些问题 背景 首先这个基于完整安装配置了anaconda和yolo11的环境,如果需要配置开始的话,先看下专栏里另一个文章。 这次的目的是实现拉取视频流…...
C++编译问题——1模板函数的实现必须在头文件中
今天编译数据结构时,遇见一个编译错误 假设你有一个头文件 SeqList.h 和一个源文件 SeqList.cpp。 SeqList.h #ifndef SEQLIST_H #define SEQLIST_H#include <stdexcept> #include <iostream>template<typename T> class SeqList { private:sta…...
深度学习PyTorch之数据加载DataLoader
深度学习pytorch之简单方法自定义9类卷积即插即用 文章目录 数据加载基础架构1、Dataset类详解2、DataLoader核心参数解析3、数据增强 数据加载基础架构 核心类关系图 torch.utils.data ├── Dataset (抽象基类) ├── DataLoader (数据加载器) ├── Sampler (采样策略)…...
使用Beanshell前置处理器对Jmeter的请求body进行加密
这里我们用HmacSHA256来进行加密举例: 步骤: 1.先获取请求参数并对请求参数进行处理(处理成String类型) //处理请求参数的两种方法: //方法一: //获取请求 Arguments args sampler.getArguments(); //转…...
前端面试:如何减少项目里面 if-else?
在前端开发中,大量使用 if-else 结构可能导致代码调试困难、可读性降低和冗长的逻辑。不妨考虑以下多种策略来减少项目中的 if-else 语句,提高代码的可维护性和可读性: 1. 使用对象字面量替代 用对象字面量来替代 if-else 语句,…...
05.基于 TCP 的远程计算器:从协议设计到高并发实现
📖 目录 📌 前言🔍 需求分析 🤔 我们需要解决哪些问题? 🎯 方案设计 💡 服务器架构 🚀 什么是协议?为什么要设计协议? 📌 结构化数据的传输问题 …...
Matlab:矩阵运算篇——矩阵数学运算
目录 1.矩阵的加法运算 实例——验证加法法则 实例——矩阵求和 实例——矩阵求差 2.矩阵的乘法运算 1.数乘运算 2.乘运算 3.点乘运算 实例——矩阵乘法运算 3.矩阵的除法运算 1.左除运算 实例——验证矩阵的除法 2.右除运算 实例——矩阵的除法 ヾ( ̄…...
git reset的使用,以及解决还原后如何找回
文章目录 git reset 详解命令作用常用参数1. --soft2. --mixed(默认参数,可省略)3. --hard4. 提交引用 总结 git reset --hard HEAD^ 还原代码如何找回?利用 git reflog找回 git reset 详解 git reset 是 Git 中一个功能强大且较…...
react中字段响应式
class中用法: import React, { Component } from react export default class Index extends Component<any, any> { constructor(props) { super(props) this.state { settingInfo: {}, } } async componentDidMount() { let settingInfo awa…...
vue中,watch里,this为undefined的两种解决办法
提示:vue中,watch里,this为undefined的两种解决办法 文章目录 [TOC](文章目录) 前言一、问题二、方法1——使用function函数代替箭头函数()>{}三、方法2——使用that总结 前言 尽量使用方法1——使用function函数代替箭头函数()…...
智能客服意图识别:结合知识库数据构建训练语料的专业流程
智能客服意图识别:结合知识库数据构建训练语料的专业流程 构建基于知识库的智能客服意图识别模型,需要综合运用 NLP(自然语言处理)、知识图谱、机器学习 等技术,确保意图识别的准确性和覆盖度。以下是专业的流程&…...
Spring Boot集成Spring Statemachine
Spring Statemachine 是 Spring 框架下的一个模块,用于简化状态机的创建和管理,它允许开发者使用 Spring 的特性(如依赖注入、AOP 等)来构建复杂的状态机应用。以下是关于 Spring Statemachine 的详细介绍: 主要特性 …...
压缩空气储能仿真simulink模型
压缩空气储能仿真simulink模型,适合matlab 2017及以上版本 CompressingGas.slx , 40474...
Tomcat 安装
一、Tomcat 下载 官网:Apache Tomcat - Welcome! 1.1.下载安装包 下载安装包: wget https://dlcdn.apache.org/tomcat/tomcat-9/v9.0.102/bin/apache-tomcat-9.0.102.tar.gz 安装 javajdk。 yum install java-1.8.0-openjdk.x86_64 -y /etc/altern…...
贪心算法和遗传算法优劣对比——c#
项目背景:某钢管厂的钢筋原材料为 55米,工作需要需切割 40 米(1段)、11 米(15 段)等 4 种规格 ,现用贪心算法和遗传算法两种算法进行计算: 第一局:{ 40, 1 }, { 11, 15…...
系统开发资源
一、前端篇 1.1 菜鸟CSS教程 1.2 HTML/CSS/JS 在线工具 二、后端篇 三、其他篇 3.1 菜鸟官网 3.2 黑马程序员学习路线 3.3 根据地区获取经纬度...
深度学习 bert与Transformer的区别联系
BERT(Bidirectional Encoder Representations from Transformers)和Transformer都是现代自然语言处理(NLP)中的重要概念,但它们代表不同的层面。理解这两者之间的区别与联系有助于更好地掌握它们在NLP任务中的应用。 …...
unity使用mesh 画图(1)
plane 圆 空心椭圆 椭圆 using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI;public class DrawMeshManager {static DrawMeshManager instance;public static DrawMeshManager Instance {get {if (instance ! null){retu…...
SpringMVC响应页面及不同类型的数据,
目录 响应页面 响应数据 文本数据 响应POJO对象 编辑 响应生命周期 视图解析器 控制器(Controller)处理完客户端请求后,生成的并返回给客户端的结果就是响应,响应的结果可以是静态页面,数据,HTM…...
地理信息系统(ArcGIS)在水文水资源及水环境中的应用:空间数据管理、空间分析功能、可视化表达
随着全球工业化和经济的快速发展,水资源短缺、水污染等问题日益严峻,成为制约可持续发展的重大瓶颈。地理信息系统(GIS)以其强大的空间数据管理和分析能力,在水文水资源及水环境的研究和管理中展现出独特优势。本文将深…...
电路原理(电容 集成电路NE555)
电容 1.特性:充放电,隔直流,通交流 2.电容是通过聚集正负电荷来存储电能的 3.电容充放电过程可等效为导通回路 4.多电容并联可以把容量叠加,但是多电容串联就不会,只会叠加电容的耐压值。 6.电容充放电时相当于通路&a…...
C++对象的初始化和对象所占资源的清理-----初始化列表
一、初始化列表 C 提供了 初始化列表(initializer list) 语法,可以在 构造函数 中用来初始化类的成员变量。它的主要优势是 提高效率,特别是在初始化 const 或 reference 类型的成员时,以及避免额外的赋值操作。 1.…...
零成本搭建Calibre个人数字图书馆支持EPUB MOBI格式远程直读
文章目录 前言1.网络书库软件下载安装2.网络书库服务器设置3.内网穿透工具设置4.公网使用kindle访问内网私人书库 前言 嘿,各位书虫们!今天要给大家安利一个超级炫酷的技能——如何在本地Windows电脑上搭建自己的私人云端书库。亚马逊服务停了ÿ…...
Ansible命令行模式常用模块使用案例(二)
在Ansible中,命令行模式(Ad-Hoc 模式)是一种快速执行任务的方式,适合临时任务或简单操作。以下是 Ansible 命令行模式中常用模块的使用案例(第二部分): 1 file模块 功能特性:主要用于…...
12. Pandas :使用pandas读Excel文件的常用方法
一 read_excel 函数 其他参数根据实际需要进行查找。 1.接受一个工作表 在 11 案例用到的 Excel 工作簿中,数据是从第一张工作表的 A1 单元格开始的。但在实际场景中, Excel 文件可能并没有这么规整。所以 panda 提供了一些参数来优化读取过程。 比如 s…...