注册

iOS标准库中常用数据结构和算法之内存池

⛲️内存池

内存池提供了内存的复用和持久的存储功能。设想一个场景,当你分配了一块大内存并且填写了内容,但是你又不是经常去访问这块内存。这样的内存利用率将不高,而且无法复用。而如果是采用内存池则可以很轻松解决这个问题:你只需要从内存池中申请这块内存,设置完内容后当不需要用时你可以将这块内存放入内存池中,供其他地方在申请时进行复用,而当你再次需要时则只需要重新申请即可。内存池提供了内存分配编号而且设置脏标志的概念,当你把分配的内存放入内存池并设置脏标志后,系统就会在适当的时候将这块内存的内容写回到磁盘,这样当你再次根据内存编号来访问内存时,系统就又会从磁盘中将内容读取到内存中去。

功能:在iOS中提供了一套内存池管理的API,你可以用这套API来实现上述的功能,而且系统内部很多功能也是借助内存池来实现内存复用和磁盘存储的。
头文件: #include <mpool.h>, #include <db.h>
平台: BSD系统,linux系统

一、内存池的创建、同步和关闭

功能:创建和关闭一个内存池对象并和磁盘文件绑定以便进行同步操作。
函数签名:

//创建一个内存池对象
MPOOL * mpool_open(void *key, int fd, pgno_t pagesize, pgno_t maxcache);

//将内存池中的脏数据同步写回到磁盘文件中
int mpool_sync(MPOOL *mp);

//关闭和销毁内存池对象。
int mpool_close(MPOOL *mp);

参数:

key:[in] 保留字段,暂时没有用处,传递NULL即可。
fd:[in] 内存池关联的磁盘文件句柄,文件句柄需要用open函数来打开。
pagesize:[in] 内存池中每次申请和分配的内存的尺寸大小,单位是字节。
maxcache:[in] 内存池中内存页的最大缓存数量。如果池中缓存的内存数量超过了最大缓存的数量就会复用已经存在的内存,而不是每次都分配新的内存。
return:[out] 返回一个新创建的内存池对象,其他两个函数成功返回0,失败返回非0.

描述:

1、内存池中的内存的分配和获取是以页为单位进行的,每次分配的页的尺寸大小由pagesize指定,同时内存池也指定了最大的缓存页数量maxcache。每次从内存池中分配一页内存时,除了会返回分配的内存地址外,还会返回这一页内存的编号。这个编号对于内存池中的内存页来说是唯一的。因为内存池中的内存是可以被复用的,因此就有可能是不同的编号的内存页所得到的内存地址是相同的。

2、每一个内存池对象都会要和一个文件关联起来,以便能够实现内存数据的永久存储和内存的复用。文件句柄必须用open函数来打开,比如下面的例子:

int fd = open("/Users/apple/mpool", O_RDWR|O_APPEND|O_CREAT,0660);

3、当我们不需要使用某个内存页时或者内存页的内容有改动则我们需要将这个内存页放入回内存池中,并将页标志为脏(DIRTY)。这样系统就会在适当的时候将此内存页的数据写回到磁盘文件中,同时此内存页也会在后续被重复利用。

4、当我们想将所有设置为脏标志的内存页立即写入磁盘时则需要调用mpool_sync函数进行同步处理。

5、当我们不再需要内存池时,则可以通过mpool_close来关闭内存池对象,需要注意的是关闭内存池并不会将内存中的数据回写到磁盘中去。

二、内存池中内存的获取

功能: 从内存池中申请分配一页新的内存或者获取现有缓存中的内存。
函数签名:

//从内存池中申请分配一页新的内存
void * mpool_new(MPOOL *mp, pgno_t *pgnoaddr);
//根据内存编号页获取对应的内存。
void * mpool_get(MPOOL *mp, pgno_t pgno, u_int flags);

参数:

mp:[in] 内存池对象。
pgnoaddr:[out] 用于mpool_new函数,用于保存新分配的内存页编号。
pngno:[in] 用于mpool_get函数,指定要获取的内存页的编号。
flags:[in] 此参数暂时无用。
return:[out] 返回分配或者获取的内存地址。如果分配或者获取失败则返回NULL。

描述:

1、无论是new还是get每次从内存池里面分配或者获取的内存页的大小都是由上述mpool_open函数中的pagesize参数指定的大小。
2、系统内部分配的内存是用calloc函数实现的,但是我们不需要手动调用free来对内存进行释放处理。
3、每个内存页都有一个唯一的页编号,而且每次分配的页编号也会一直递增下去。
4、mpool_new函数申请分配新的内存时,如果当前缓存中的内存页小于maxcache数量则总是分配新的内存,只有当缓存数量大于maxcache时才会从现有的缓存中寻找一页可以被重复利用的内存页,如果没有可以重复利用的页面,则会继续分配新的内存页。
5、mpool_get函数则根据内存页的编号获取对应的内存页。如果编号不存在则返回NULL。需要注意的是一般在获取了某一页内存后,不要进行重复获取操作,否则在DEBUG状态下会返回异常。另外一个情况是有可能相同的页编号下两次获取的内存地址是不一样的,因为系统实现内部有内存复用的机制。

三、内存池中内存的放回

功能:将分配或者申请的内存页放回到内存池中去,以便进行重复利用。
函数签名:

int  mpool_put(MPOOL *mp, void *pgaddr, u_int flags);

参数:

mp: [in] 内存池对象。
pgaddr:[in] 要放入缓存的内存页地址。这个地址由mpool_get/new两个函数返回。
flags:[in] 放回的属性,一般设置为0或者MPOOL_DIRTY。
return:[in] 函数调用成功返回0,失败返回非0

描述:

1、这个函数用来将内存页放入回内存池缓存中,以便对内存进行重复利用。当将某个内存地址放入回缓存后,将不能再次访问这个内存地址了。如果要想继续访问内存中的数据则需要借助上述的mpool_get/new函数来重新获取。
2、flags:属性如果指定为0时,表明放弃这次内存中的内容的修改,系统不会将内存中的内容写入到磁盘中,而只是将内存放入缓存中供其他地方重复使用。而如果设置为MPOOL_DIRTY时,则表明将这页内存中的数据设置为脏标志,除了同样将内存放入缓存中重复利用外,则会在适当的时候将内存中的数据写入到磁盘中,以便下次进行读取。

四、内存池磁盘读写通知

功能:注册回调函数,当某页内存要写回到磁盘或者要从磁盘中读取时就会调用指定的回调函数。
函数签名:

void mpool_filter(MPOOL *mp, void (*pgin)(void *, pgno_t, void *),
void (*pgout)(void *, pgno_t, void *), void *pgcookie);

参数:

mp:[in] 内存池对象.
pgin: [in]: 回调函数,当某个内存页的数据需要从磁盘读取时,会在读取完成后调用这个回调函数。
pgout:[in]: 回调函数,当某个内存页的数据要到磁盘时,会在写入完成后调用这个回调函数。
pgcookie: [in] 上述两个回调函数的附加参数。

描述:

因为内存池中的内存页会进行复用,以及会在适当的时候将内容同步到磁盘中,或者从磁盘中将内容读取到内存中,因此可以借助这个函数来监控这些磁盘文件和内存之间的读写操作。pgin和pgout函数的格式定义如下:

//pgin和pgout回调函数的格式。
//pgcookie:是mpool_filter函数中传入的参数。
//pgno: 要进行读写的内存页编号
//pageaddr: 要进行读写的内存地址。
void (*pgcallback)(void *pgcookie, pgno_t pgno, void *pageaddr);

五、实例代码

#include <mpool.h>
#include <db.h>

//创建并打开一个文件。
int fd = open("/Users/apple/mpool", O_RDWR|O_APPEND|O_CREAT,0660);

//创建一个内存池对象,每页的内存100个字节,最大的缓存数量为4
MPOOL *pool = mpool_open(NULL, fd, 100, 4);


//从内存池中分配一个新的内存页,这里对返回的内存填写数据。
pgno_t pidx1, pidx2 = 0;
char *mem1 = (char*)mpool_new(pool, &pidx1);
memcpy(mem1, "aaa", 4);

char *mem2 = (char*)mpool_new(pool, &pidx2);
memcpy(mem2, "bbb", 4);

//将分配的内存mem1放回内存池中,但是内容不保存到磁盘
mpool_put(pool, mem1, 0);
//将分配的内存mem2放回内存池中,但是内容保存到磁盘。
mpool_put(pool, mem2, MPOOL_DIRTY);

//经过上面的操作后mem1,mem2将不能继续再访问了,需要访问时需要再次调用mpool_get。
mem1 = (char*)mpool_get(pool, pidx1, 0);
mem2 = (char*)mpool_get(pool, pidx2, 0);

//上面的mem1和mem2可能和前面的new返回的地址是不一样的。因此在内存池中不能通过地址来做唯一比较,而应该将编号来进行比较。

//将所有设置为脏标志的内存也写回到磁盘中去。
mpool_sync(pool);

mpool_close(pool); //关闭内存池。

close(fd); //关闭文件。

内存池为iOS系统底层开发提供了一个非常重要的能力,我们可以好好利用内存池来对内存进行管理,以及一些需要进行持久化的数据也可以借助内存池来进行保存,通过内存池提高内存的重复利用率。

转自:https://www.jianshu.com/p/34bd3e5c5b4e

0 个评论

要回复文章请先登录注册