SSブログ

libapr の話(その2) + 他

○ はじめに

 前回にひきつづいて libapr のプログラミングチュートリアルを見てゆく。

原書 : http://dev.ariel-networks.com/apr/apr-tutorial/html/apr-tutorial-3.html
著者 : INOUE Seiichiro inoue@ariel-networks.com


○ 本文
【memory pool】

 libapr のほとんどの API はメモリプールに依存している。ユーザにとってはメモリプールによって、複数のメモリ領域を管理する事が容易になる。
 ここでメモリプールが無い場合について考えてみる。ユーザが何回かメモリ確保を行った場合、ユーザはその数だけ取得したメモリ領域の開放処理を行わなければならない。
 例えば、メモリ領域を 10 回取得したならば、10 回メモリ開放処理を行わなければならない。さもなければ、メモリリークが生じてしまう。
メモリプールはこの問題を解消する。ユーザが一度メモリプールを確保したならば、そこから複数のメモリ領域を取得する事が出来る。これらのメモリ領域を開放する為に、ユーザはメモリプール自体を開放するだけでよい。
 これには 2 つの利点がある。一つは、上述したようなメモリリークの問題を防ぐこと。もう一つは、メモリ確保に伴うコストが比較的少ない事である。
 ある意味で、ユーザはセッション・コンテキストに準拠したプログラミングを行う事になる。というのも、メモリプールは同じ寿命のオブジェクトの集合であるセッション・コンテキストの一種と言えるためだ。ユーザはセッションが張られている間だけ、オブジェクトの集合を操作できる。
 セッションの開始時に、ユーザはメモリプールを作成し、ユーザはセッションが張られている間にオブジェクトを作成する。このとき、ユーザは各オブジェクトの寿命を意識する必要は無い。そして最後に、メモリプールを削除する。

注:一般的に、オブジェクトの寿命を管理する事はプログラミングにおいて最も難しい事である。なのでこの他にも、スマートポインタやガベージコレクションなどさまざまな手法が存在する。ちなみに、これらの手法を同時に使うのは難しい。メモリプールもメモリの寿命管理を行う技術の一つなので、他の技術との併用には十分注意が必要だ。

注:将来的に、メモリプールは今後廃れていくと予想される。こちらを参照されたし
http://mail-archives.apache.org/mod_mbox/apr-dev/200502.mbox/%3c1f1d9820502241330123f955f@mail.gmail.com%3e

 libapr にはメモリプールに関して、次の 3 つの基本的な API が存在する。


/* excerpted from apr_pools.h */

APR_DECLARE(apr_status_t) apr_pool_create(apr_pool_t **newpool,
apr_pool_t *parent);
APR_DECLARE(void *) apr_palloc(apr_pool_t *p, apr_size_t size);
APR_DECLARE(void) apr_pool_destroy(apr_pool_t *p);



 メモリプールは apr_pool_create() を呼び出す事で作成され、apr_pool_destroy() が呼ばれるまで存在し続ける。apr_pool_create() の第一引数に、結果を格納するオブジェクトのポインタを渡す。そして、API から処理が返ると、こいつに新しく作成されたメモリプールのオブジェクト (apr_pool_t) のアドレスが格納される。
 又、apr_palloc() を呼び出し、引数にプールする各メモリ領域のサイズを指定しても、メモリプールを作成できる。使い方については mp-sample.c を参照されたし。


/* excerpted from mp-sample.c */

apr_pool_t *mp;
/* create a memory pool. */
apr_pool_create(&mp, NULL);

/* allocate memory chunks from the memory pool */
char *buf1;
buf1 = apr_palloc(mp, MEM_ALLOC_SIZE);



 より簡単に使えるものとして、malloc(3) に似た apr_palloc() というモノが使える。メモリプール使用に際して、ユーザは apr_palloc() も使わなければならない。察しのように、malloc(3) と使い方がよく似ている。
 apr_palloc() は、0 で初期化されたメモリ領域を返す。ユーザが malloc(3) や calloc(3) を使った場合には、free(3) を呼び出さなければならないが。この場合においても、各メモリ領域に対して個別にメモリ開放処理を行う必要は無く、apr_pool_destroy() を呼び出す事で、メモリプールに存在する全てのメモリ領域を開放する。

注:apr_palloc() によって指定される、プールする各メモリ領域のサイズに特に制限は損ザいしないが、大きいサイズのメモリ領域を指定する事は、あまりよろしくない。というのも、メモリプールは基本的に、小さいサイズのメモリ領域をプールするように設計されているからである。実際、メモリプールの初期化サイズは 8 KB である。もし、数メガを越えるような大きなサイズのメモリ領域が必要ならば、メモリプールは使わない方が良い。

注:原則、メモリプールは一度確保したメモリ領域をシステムに対して返却する事はしない。なので、もし長い間プログラムが起動していた場合には問題が起きるので、以下のように上限値を指定したほうがいい。


/* sample code to set the upper limit to make memory pool manager release the memory back to the system */
#define YOUR_POOL_MAX_FREE_SIZE 32 /* apr_pool max free list size */

apr_pool_t *mp;
apr_pool_create(&mp, NULL);
apr_allocator_t* pa = apr_pool_allocator_get(mp);
if (pa) {
 apr_allocator_max_free_set(pa, YOUR_POOL_MAX_FREE_SIZE);
}



 あともう 2 つ、知らなければならない API が存在する。apr_pool_clear() と apr_pool_cleanup_register() である。
 apr_pool_clear() は、 atpr_pool_destroy() と似ているが、メモリプール自体は呼出し後も利用可能である。以下に使用例を示す。


/* sample code about apr_pool_clear() */
apr_pool_t *mp;
apr_pool_create(&mp, NULL);
for (i = 0; i < n; ++i) {
 do_operation(..., mp);
 apr_pool_clear(mp);
}
apr_pool_destroy(mp);



 この例では、作成処理済みのメモリプールは do_operation() 内部で使われる。もし do_operation() 外部でプールしたメモリ領域を使わない場合、apr_pool_clear() を呼び出せる。このとき、ローカル変数のようにメモリプールのメモリ容量を減らす事ができる(*1)。つまり、apr_palloc() でスタックポインタの退避を行い、apr_pool_clear() で退避したスタックポインタを復帰するイメージである。これらの処理は非常に軽量である。

 又、apr_pool_cleanup_register() 関数によって、メモリの取得及び開放処理時に処理をフックする事ができる。つまり、メモリプールの作成及び削除時に自前のコールバック関数を登録する事で、各メモリプールに応じたデストラクタ処理を挟んだりできる。

 メモリプールにおける最後の話は、サブプールである。全てのメモリプールは自分自身の親となるメモリプールを指定できる。これによって、ユーザはツリー構造を持ったメモリプールを持つことができる。この親となるメモリプールは、メモリプール作成時に呼び出す apr_pool_create() の第二引数で指定される。これに NULL をいれた場合、新区作成されたメモリプールは、ルートのメモリプールとなる。そして、ユーザはルートの配下にメモリプールを作成していく事ができる。
 ユーザが子を持つメモリプールを削除する際、下位のメモリプールも同時に削除される事になる。又、同じ構造のメモリプールに対して、apr_pool_clean() 関数を呼び出した場合、指定されたメモリプールは残るが、下位のメモリプールは破棄される。そして、上位のメモリプールに対しても、cleanup の処理が及ぶ。

注:次の処理は、apr_pool_cleanup_register() にコールバック関数として NULL を指定するという典型的なバグである。こうではなく、何もしない処理を明示する際には、apr_pool_cleanup_null を指定する。
  

/* pseudo code about memory pool typical bug */
/* apr_pool_cleanup_register(mp, ANY_CONTEXT_OF_YOUR_CODE, ANY_CALLBACK_OF_YOUR_CODE, NULL); THIS IS A BUG */
/* FIXED */
apr_pool_cleanup_register(mp, ANY_CONTEXT_OF_YOUR_CODE, ANY_CALLBACK_OF_YOUR_CODE, apr_pool_cleanup_null);



=== オレ流解釈 ===

 (*1) 本文で "ローカル変数" といったものは、原文では "local stack memory" とある。libapr において、メモリプールでのメモリ領域の使われ方がどうなっているのかは理解していないが。ここで言いたい事は、関数からの呼び出しから帰ってきたとき、スタック領域には do_operation 関数内で利用されたローカル変数が格納されており、これらの値はもはや再利用される事は通常ありえない。libapr において do_operation 内部でプールされたメモリ領域を一時変数などで利用された場合も同様に、これらの値は今後参照される可能性は無い。なので、apr_pool_clean() 関数では、メモリプール自体を残しつつ、これらの不要な領域を未使用の状態にする。という事を言っている (のだと思う)。
 
 
 
○ おわりに

 libapr を特別な何かかと思っていたが。これも単なるライブラリの一つであると気がつくのに時間がかかった。別段 libapr を軽んじているわけではないが、これが無ければ何もできないという程のものでもない。自分はこれまで libapr を用途不明の後者のような存在だと思っていた。
 これを知るきっかけとなった "メモリプール" という仰々しい名前のオブジェクトの存在が自分にそのような印象をあたえたわけだが。よくよく考えると、glib にもユーザからは抽象化されているが "アリーナ" というメモリ区分が存在するわけだ。
 どうやら、自分という人間は本質を見抜く力が弱いようである。

 この記事もしくは、原書だけを読んで「なるほどメモリプールとはこんなものか」と思える人はすごいと思う。
 原書をまじまじと読んでも、メモリプールを使いこなせるとは思えない。実際ここに書かれている内容は、単なるメモリプールの概論に過ぎないわけで、ここからメモリプールの使いかたや、他のソースでどのように使われているのかといった事は見えてこない。
 前の章にも書かれていたように、ライブラリを理解するには使ってみるのが効果的である (らしい) ので、同技術コラムから落とせるサンプルコード (mp-sample.c 辺り) をコンパイルして動かしてみて、実際にどう使えるのか、どう使われているのかを動かしながら調べてゆく事にする。
 個人的にはサンプルコードを動かせただけで、学習意欲というか、調べてみようという気がそそられる (結果的に、物事を納められたかは別問題な気もするが)。

 最後に。ビルド方法について、書かれていないので簡単に話をする。

1. libapr のダウンロード・コンパイル

$ ./configure --prefix=/usr/local
$ make
# make install

2. /usr/local/apr/include/apr-1 を C_INCLUDE_PATH に入れる
3. /usr/local/apr/lib を /etc/ld.so.conf に追加、ライブラリ・キャッシュの再構築
4. ソースのコンパイル。リンカにライブラリを指定。

$ gcc ...c -lapr-1

 -l オプションについては、オンラインマニュアルを参照。以下、オンラインマニュアルの一部を抜粋
『リンカは、標準のライブラリ用ディレクトリのリスト中から、実際のファイル名が ‘liblibrary.a’ であるファイルを検索します。リンカはこのファイルを、ファイル名で直接指定した場合と同様に使用します。』

nice!(0)  コメント(0)  トラックバック(0) 

nice! 0

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

トラックバック 0

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。