aprのメモリプールについて調べてみました。
apr_palloc
allocator_alloc
malloc
という呼び出し階層により、結局はmallocが呼ばれます。
で、どういうタイミングでmallocが呼ばれるのか?1回のmallocで確保されるサイズは?
apr_palloc(pool, n)という呼び出しをした場合、
前回確保したブロックにnバイトの空き領域があれば、そのブロック内から切り出して返します(in apr_palloc):
/* If the active node has enough bytes left, use it. */
if (size <= node_free_space(active)) {
mem = active->first_avail;
active->first_avail += size;
return mem;
}
この記事内ではブロックという用語を使っていますが、apr_memnode_t
にほぼ1対1に対応します。aprのソースコードのコメント内でnodeと呼ばれているものと同じです。
前回のブロック内に空き領域が無い、またはn > 8192の場合は、nを4096の倍数に丸めてmallocします(in allocator_alloc)。
8192という数値は、apr_pools.cで定義されている
#define MIN_ALLOC 8192
のようです。
フリーリストは4096の倍数ごとに20個あります:
/**
* Lists of free nodes. Slot 0 is used for oversized nodes,
* and the slots 1..MAX_INDEX-1 contain nodes of sizes
* (i+1) * BOUNDARY_SIZE. Example for BOUNDARY_INDEX == 12:
* slot 0: nodes larger than 81920
* slot 1: size 8192
* slot 2: size 12288
* ...
* slot 19: size 81920
*/
apr_memnode_t *free[MAX_INDEX];
検証用に使ったソースコード
https://github.com/aoyama-val/apr_samples/tree/master/2
(LD_PRELOADを使ってmallocにprintfを仕込んでます)
実行すると結果はこうなります:
malloc(1024) = 0x55a223d38260
main
malloc(192) = 0x55a223d38670
malloc(16384) = 0x55a223d38740
malloc(16384) = 0x55a223d3c750
malloc(16384) = 0x55a223d40760
calling apr_palloc(1)
calling apr_palloc(2)
calling apr_palloc(4)
calling apr_palloc(8)
calling apr_palloc(16)
calling apr_palloc(32)
calling apr_palloc(64)
calling apr_palloc(128)
calling apr_palloc(256)
calling apr_palloc(512)
calling apr_palloc(1024)
calling apr_palloc(2048)
calling apr_palloc(4096)
calling apr_palloc(8192)
malloc(16384) = 0x55a223d44770
calling apr_palloc(16384)
malloc(20480) = 0x55a223d48780
calling apr_palloc(32768)
malloc(36864) = 0x55a223d4d790
calling apr_palloc(65536)
malloc(69632) = 0x55a223d567a0
calling apr_palloc(131072)
malloc(135168) = 0x7fbb4237b010
calling apr_palloc(262144)
malloc(266240) = 0x7fbb42335010
calling apr_palloc(524288)
malloc(528384) = 0x7fbb422b3010
8192バイト、思っていたより小さかったことがわかりました(もっと大きくガバッと取っているのかと思ってました)。
そして、これを調べるために最初ソースコードを読んで分からず、次にgdbで動かして分からず。printfを仕込むのが一番効果的でした。
allocator
poolとは別に、allocatorという概念があります。
apr_pool_createすると、通常はglobal_allocatorが使われます。
global_allocatorは、apr_pool_initialize時に作成され、apr_pool_terminate時に破棄されます。つまり、poolの寿命より長いです。
allocatorにはownerとなるpoolがあります。つまりallocatorはあるpoolに「所有」されます。owner poolが破棄されるとき、allocatorも破棄されます。
poolの破棄
apr_pool_destroyを呼ぶとpoolが破棄されます。
poolが確保していたブロックはallocatorのフリーリストに追加され、後にapr_pool_createしたときに再利用されます。
poolの親子関係
poolには親子関係があります。親のpoolが破棄されると、同時に子のpoolも破棄されます。
初期化から終了までの流れ
apr_initialize
apr_pool_initialize
- global_allocatorの作成
- global_poolの作成
apr_pool_create
- 第2引数で指定されたpool(NULLならglobal_pool)の子としてpoolを作成。global_allocatorを使う。
apr_palloc
- 必要に応じてmallocする。または既にmallocしてあるブロックから要求されたメモリを切り出して返す
apr_pool_destroy
- ブロックをallocatorのフリーリストに返す
- このpoolがallocatorのownerであれば、apr_allocator_destroyを呼ぶ
- mallocしてあるブロックをfreeする
apr_terminate
apr_pool_terminate
- global_poolの破棄(おそらくこのときglobal_allocatorも破棄される)