从golang的sync.pool到linux的slab分配器
最近学习golang的时候,看到golang并发编程中有一个sync.pool,即对象池,猛地一看这不跟linux的slab分配器类似嘛,赶紧学习记录下
这里先总结下设计sync.pool和slab的目的
- sync.pool
- 为了缓解特定类型的对象频繁创建和销毁,导致gc压力大的问题
- slab
- linux使用伙伴系统管理内存,伙伴系统分配内存时以页Page(4K byte)为单位进行分配,而linux内核中,会频繁的创建struct page, struct fd,struct task的实例,这些结构体对象的size通常只有几十个字节,如果每次都从伙伴系统分配内存,将造成极大的内存浪费
sync.pool
参考了:
基本用法:https://geektutu.com/post/hpg-sync-pool.html
基本用法+源码分析:https://www.cnblogs.com/qcrao-2018/p/12736031.html
底层分析更全面:https://www.cyhone.com/articles/think-in-sync-pool/
sync.pool的特点
并发安全并且lock-free,sync.pool之所以能做到lock-free,跟golang的GMP调度模型有关系,下面再进行详细介绍
sync.pool的使用场景
当存在对象被频繁的重复分配时,可以使用sync.pool避免对象重复创建和销毁,减轻gc的压力
sync.pool的基本用法
创建对象池
var studentPool = sync.Pool{New: func() interface{} { return new(Student) },
}
理解:创建一个sync.pool对象,命名为studentPool,创建sync.pool对象时,传入的New成员是一个函数,该函数定义了创建student对象的行为。需要注意的是,sync.pool在初始化时只会创建一个对象,后续调用Get发现没有对象时,才会继续创建对象
向对象池添加/获取一个对象
stu := studentPool.Get().(*Student)
json.Unmarshal(buf, stu)
studentPool.Put(stu)
完整的例子
package mainimport ("fmt""sync"
)var pool *sync.Pooltype Person struct {Name string
}func initPool() {pool = &sync.Pool{New: func() interface{} {fmt.Println("Creating a new Person")return new(Person)},}
}func main() {initPool()p := pool.Get().(*Person)fmt.Println("首次从 pool 里获取:", p)p.Name = "first"fmt.Printf("设置 p.Name = %s\n", p.Name)pool.Put(p)fmt.Println("p.name:", p.Name) // p仍然有效p1 := pool.Get().(*Person)fmt.Println("p1.name:", p1.Name)fmt.Println("Pool 没有对象了,调用 Get: ", pool.Get().(*Person))
}
输出
Creating a new Person
首次从 pool 里获取: &{}
设置 p.Name = first
p.name: first
p1.name: first
Creating a new Person
Pool 没有对象了,调用 Get: &{}
从这个例子可以看出,当p被存入pool后,p仍然是有效的,并且p的字段并没有因为p存入pool而被清空
sync.pool底层实现
sync.pool结构体
type Pool struct {noCopy noCopy// 每个 P 的本地队列,实际类型为 [P]poolLocallocal unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal// [P]poolLocal的大小localSize uintptr // size of the local arrayvictim unsafe.Pointer // local from previous cyclevictimSize uintptr // size of victims array// 自定义的对象创建回调函数,当 pool 中无可用对象时会调用此函数New func() interface{}
}
上文提到,sync.pool是lock-free的,关键就在于这里的local是一个[GOMAXPROCS]poolLocal数组,由于每个 P 都有自己的一个本地对象池 poolLocal,Get 和 Put 操作都会优先存取本地对象池。由于 P 的特性,操作本地对象池的时候整个并发问题就简化了很多,可以尽量避免并发冲突
poolLocal定义如下
type poolLocal struct {poolLocalInternal// 将 poolLocal 补齐至两个缓存行的倍数,防止 false sharing,// 每个缓存行具有 64 bytes,即 512 bit// 目前我们的处理器一般拥有 32 * 1024 / 64 = 512 条缓存行// 伪共享,仅占位用,防止在 cache line 上分配多个 poolLocalInternalpad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}// Local per-P Pool appendix.
type poolLocalInternal struct {// P 的私有缓存区,使用时无需要加锁private interface{}// 公共缓存区。本地 P 可以 pushHead/popHead;其他 P 则只能 popTailshared poolChain
}
poolChain 是一个双端队列的实现
type poolChain struct {// 只有生产者会 push to,不用加锁(Get不会触发对象窃取)head *poolChainElt// 读写需要原子控制。 pop fromtail *poolChainElt
}type poolChainElt struct {poolDequeue// next 被 producer 写,consumer 读。所以只会从 nil 变成 non-nil// prev 被 consumer 写,producer 读。所以只会从 non-nil 变成 nilnext, prev *poolChainElt
}type poolDequeue struct {// The head index is stored in the most-significant bits so// that we can atomically add to it and the overflow is// harmless.// headTail 包含一个 32 位的 head 和一个 32 位的 tail 指针。这两个值都和 len(vals)-1 取模过。// tail 是队列中最老的数据,head 指向下一个将要填充的 slot// slots 的有效范围是 [tail, head),由 consumers 持有。headTail uint64// vals 是一个存储 interface{} 的环形队列,它的 size 必须是 2 的幂// 如果 slot 为空,则 vals[i].typ 为空;否则,非空。// 一个 slot 在这时宣告无效:tail 不指向它了,vals[i].typ 为 nil// 由 consumer 设置成 nil,由 producer 读vals []eface
}
sync.pool Get接口调用时,可能会触发对象窃取,poolDeque可能被多个P访问,sync.pool内部使用CAS原子操作保证并发安全性
slab分配器
参考:https://segmentfault.com/a/1190000043626203
为什么需要slab分配器
linux伙伴系统管理内存的最小单位是物理内存页page,如果为几十个或者几百个字节的分配请求分配一整个页,将造成大量内存
slab分配器在内核代码中使用
对于用户空间,glibc的malloc内部维护一个空闲链表
使用slab分配器的好处
缓存友好
利用 CPU 高速缓存提高访问速度。当一个对象被直接释放回 slab 对象池中的时候,这个内核对象还是“热的”,仍然会驻留在 CPU 高速缓存中。如果这时,内核继续向 slab 对象池申请对象,slab 对象池会优先把这个刚刚释放 “热的” 对象分配给内核使用,因为对象很大概率仍然驻留在 CPU 高速缓存中,所以内核访问起来速度会更快
弥补伙伴系统调用路径长和只能按照整个物理页分配的缺点
伙伴系统只能分配 2 的次幂个完整的物理内存页,这会引起占用高速缓存以及 TLB 的空间较大,导致一些不重要的数据驻留在 CPU 高速缓存中占用宝贵的缓存空间,而重要的数据却被置换到内存中。 slab 对象池针对小内存分配场景,可以有效的避免这一点
slab分配器使用场景
主要是分配如下结构体对象的场景
-
struct task_struct
-
struct mm_struct
-
struct fd
-
struct page
-
struct socket
对于每种结构体,内核会创建一个专属的slab分配器
slab底层原理
暂时只需要了解slab是什么,和slab的基本概念。前面的区域,后面再探索吧~