skynet 在默认情况下使用了 jemalloc 进行内存分配,这种情况下 skynet 提供了自定义的内存管理函数,本篇会尝试讲解这部分功能的实现。
接口
|
|
这一套接口跟标准库的接口命名完全一样,只是在名字前面加了 skynet_ 的前缀,同时参数和功能也是完全一样的。
这里有一个小技巧,除了声明了这些函数以外,每个接口还有一个同名的 define 定义,如果加了编译选项 NOUSE_JEMALLOC 的话,则这些函数声明都不会有实现,这时候调用这些函数实际就是使用的同名的 define 定义。
结构
|
|
上面是内存数据相关的记录,_used_memory 是本进程内通过 skynet_malloc 分配的所有内存数量,_memory_block 是本进程内通过 skynet_malloc 分配的内存块数,跟分配次数相等。
mem_data 是针对服务的,每个服务都会有一个 mem_data 结构的变量在 mem_stats 里面保存。handle 用来保存服务的 handle,allocated 用来保存该服务已分配的内存字节数。这里有个小问题,就是 SLOT_SIZE 是小于 handle 的增长上限的,而且数组长度也不会增长,所以并不是所有服务都可以统计分配的内存数量。
mem_cookie 是每次分配内存的时候都会在分配出的内存块后面加上这样一块后缀数据。如果不开 MEMORY_CHECK 的话,也会记录,不过只会有 handle 的记录。
实现
分配
分配的接口 skynet_malloc 实现很简单,只有三步操作。首先需要调用 je_malloc 分配一块需求长度 + 后缀长度的内存。然后检查分配结果,如果分配失败则触发 OOM 处理输出错误信息,执行 abort 结束进程。如果分配成功,则为内存块填充后缀,填充完毕以后修改上面提到的各种数据计数。
如果要使用 skynet_realloc 进行内存的重分配的话,也不会太麻烦,相比 skynet_malloc 只是多了一步要先清除就的后缀的操作。
释放
调用 skynet_free 可以释放内存。释放内存的主要额外步骤就是要清除后缀了。清除后缀其实并不是真的为了清除后缀,因为后缀不用清除,直接释放即可。
清除后缀真正做的事情是为了修改分配的是计数,这里要先通过后缀拿到服务的 handle 才行。拿到以后即可将对应的内存分配数据减少。
检测
skynet 提供了一个简单的内存检测功能,可以通过定义 MEMORY_CHECK 开启。提供了 double free 和 out of bound 的检测。
检测的原理主要是通过检测 mem_cookie 的 dogtag 的值来判断的。在内存分配的时候,如果开启 MEMORY_CHECK 的情况下,dogtag 会被赋值为 MEMORY_ALLOCTAG,其余并无额外的操作。
在内存释放的时候,如果 dogtag 不是 MEMORY_ALLOCTAG,则说明之前的 dogtag 被覆盖掉了,则可以看作是内存越界写入了。如果是 MEMORY_ALLOCTAG 则将 dogtag 设为 MEMORY_FREETAG 来标记它已经被释放了。同时在一开始也会检查 dogtag 是否是 MEMORY_FREETAG,如果是的话,则可以说明存在二次释放的问题。