nginxソースコードリーディング その4〜コアAPI(メモリプール)〜

  • 13
    Like
  • 0
    Comment
More than 1 year has passed since last update.

しばらくコアAPIの話が続きます。

前回

nginxソースコードリーディング その3〜コアAPI(文字列)〜

リビジョン

5428:fcecb9c6a057

メモリプール

CのようにGCを持たない言語ではヒープから確保したメモリの開放はプログラマの責任で行う必要がある。しかし、実際にCやC++でプログラミングしたことがあるならわかるように確保したメモリを適切なタイミングで解放するのはとても難しいとまでは言わないまでもあまり簡単なことではない。

単にfreeを呼び出すのを忘れたり、もしくはリストやツリーのような少しでも複雑なデータ構造を扱う際に割り当てられたメモリを適切に解放できずにリークしてしまうようなことは普通に起こりうる。

モダンなOSではプログラム終了後にそのプログラムに割り当てられたメモリをOS側で解放してくれるので、走らせてすぐ終了するようなプログラムではこのようなメモリリークは特に問題にならないこともあるが、nginxのように常時動き続けるようなソフトウェアでのメモリリークは致命的な問題を引き起こす。

Cプログラムでこの問題に対処するためのアプローチの一つがメモリプールという手法である。

メモリプールは簡単に言うとまずヒープからあらかじめ大きめのメモリ(プール)を確保しておき、
メモリが必要な際にそのプールから必要な領域を割り当てる。こうすることでメモリの確保と開放を行う箇所を一箇所にまとめることができる。

また、単純にアロケーション(mallocを呼び出す)の頻度が減るので一般にパフォーマンスが良くなると考えられている。

純粋にメモリプールの話をやっていると非常に長くなるのでnginxの話に戻ろう。
よりメモリプールについて知りたい人は僕が昔書いた資料があるのでそちらを参照されたし。

nginxのメモリプール

実際のnginxのメモリプールを使ったコードはイメージとしてはこんな感じ。

pool = ngx_create_pool(NGX_CYCLE_POOL_SIZE, log);
                          
p1 = ngx_palloc(pool, siz1);
p2 = ngx_palloc(pool, siz2);
p3 = ngx_palloc(pool, siz3);
                          
ngx_destroy_pool(pool);

最初にメモリプールを作成(ngx_create_pool)した後、そのメモリプールから必要なメモリを割り当てる(ngx_palloc)。最後に必要なくなった時点でメモリプール毎開放(ngx_destroy_pool)する。nginxでこの方法が有効な場面は例えば拡張モジュールの開発が挙げられる。

拡張モジュールの開発は詰まるところ、nginxのリクエストの各処理フェーズ(rewrite, access, log等)にフックをかけるハンドラ関数を書くことだが、リクエスト処理の開始時にngx_create_poolを呼び出し、終了時にngx_destroy_poolを呼び出すようにしておけば、拡張モジュールを開発する際に動的なメモリ確保が必要な場合はそこのメモリプールから割り当てるだけでよい。

あとはnginxがよしなにやってくれる。実際、ngx_http_request_tというクライアントからのリクエストを表す構造体にはngx_pool_t型のメンバが含まれており、各フックへのハンドラ関数はこの構造体へのポインタを引数に取るので初期化済みのメモリプールが使える。

メモリプールのデータ構造

nginxのメモリプールのデータ構造は次のように定義されている。

core/ngx_palloc.h
struct ngx_pool_large_s {
    ngx_pool_large_t     *next;
    void                 *alloc;
};

typedef struct {
    u_char               *last;
    u_char               *end;
    ngx_pool_t           *next;
    ngx_uint_t            failed;
} ngx_pool_data_t;

struct ngx_pool_s {
    ngx_pool_data_t       d;
    size_t                max;
    ngx_pool_t           *current;
    ngx_chain_t          *chain;
    ngx_pool_large_t     *large;
    ngx_pool_cleanup_t   *cleanup;
    ngx_log_t            *log;
};

struct ngx_pool_sにメモリプールの状態や操作に必要な情報が、ngx_pool_data_tには実際のメモリブロックへのポインタが入っている。struct ngx_pool_large_sは作成したメモリプールよりも大きなメモリブロックを確保しようとした時に利用する。各メンバが何を意味しているのか理解するために実装を細かく見ていく。

ngx_create_pool

まずはngx_create_poolの実装を見ていく。

core/ngx_palloc.c
ngx_pool_t *
ngx_create_pool(size_t size, ngx_log_t *log)
{
    ngx_pool_t  *p;

    p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log);
    if (p == NULL) {
        return NULL;
    }

    p->d.last = (u_char *) p + sizeof(ngx_pool_t);
    p->d.end = (u_char *) p + size;
    p->d.next = NULL;
    p->d.failed = 0;

    size = size - sizeof(ngx_pool_t);
    p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;

    p->current = p;
    p->chain = NULL;
    p->large = NULL;
    p->cleanup = NULL;
    p->log = log;

    return p;
}

ngx_memalignが実際のメモリ確保を行う関数。mallocでないのは16(NGX_POOL_ALIGNMENT)の倍数のアドレスでアラインメントするためなんだろうけど、これは何故だろ?(一般的なコンピュータ(例:i386, amd64)上ではglibcのmallocは8の倍数のアドレスを返す)。

まだ利用目的がわからないメンバもあるが、これを見るだけで半分くらいは理解できた。

core/ngx_palloc.h
struct ngx_pool_large_s {
    ngx_pool_large_t     *next;    /* まだわからない                         */
    void                 *alloc;   /* まだわからない                         */
};

typedef struct {
    u_char               *last;    /* メモリブロックの現在位置               */
    u_char               *end;     /* メモリブロックの終了位置               */
    ngx_pool_t           *next;    /* 次のメモリブロックへのポインタ         */
    ngx_uint_t            failed;  /* まだわからない                         */
} ngx_pool_data_t;

struct ngx_pool_s {
    ngx_pool_data_t       d;       /* 実際のメモリブロック                   */
    size_t                max;     /* メモリプールの最大メモリサイズ         */
    ngx_pool_t           *current; /* 現在利用中のメモリブロックへのポインタ */
    ngx_chain_t          *chain;   /* まだわからない                         */
    ngx_pool_large_t     *large;   /* まだわからない                         */
    ngx_pool_cleanup_t   *cleanup; /* まだわからない                         */
    ngx_log_t            *log;     /* エラー等の出力先のログ                 */
};

このことからnginxのメモリプールは複数のメモリブロックをリストで管理していることがわかる。
そして今利用しているメモリブロックの空きが足りなくなったら新しいメモリブロックを作成してそれをリストにつなぐ。
こうしておくことでメモリプールから確保したデータ領域はすべてこのリストを辿るだけで行える。

ngx_palloc

続いてngx_palloc。メモリプールからデータ領域を確保する関数はほかにもあるが、これが一番普通なのでこれを元に解説する。

core/ngx_palloc.c
void *
ngx_palloc(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    ngx_pool_t  *p;

    if (size <= pool->max) {

        p = pool->current;

        do {
            m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT);

            if ((size_t) (p->d.end - m) >= size) {
                p->d.last = m + size;

                return m;
            }

            p = p->d.next;

        } while (p);

        return ngx_palloc_block(pool, size);
    }

    return ngx_palloc_large(pool, size);
}

確保するサイズがpool->max以下の場合はそのプールのメモリブロックから領域を確保する。
その後、空いているメモリブロックを探し、空きが見つかったらそのブロックの現在位置のアドレスを
unsigned long(NGX_ALIGNMENT)のサイズ(OS依存)のバイト境界にアラインメントして返す。
空きがない場合はngx_palloc_blockを呼んで新たなメモリブロックを作成する。

ngx_palloc_block

続いてngx_palloc_block。

core/ngx_palloc.c
static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    size_t       psize;
    ngx_pool_t  *p, *new, *current;

    psize = (size_t) (pool->d.end - (u_char *) pool);

    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
    if (m == NULL) {
        return NULL;
    }

    new = (ngx_pool_t *) m;

    new->d.end = m + psize;
    new->d.next = NULL;
    new->d.failed = 0;

    m += sizeof(ngx_pool_data_t);
    m = ngx_align_ptr(m, NGX_ALIGNMENT);
    new->d.last = m + size;

    current = pool->current;

    for (p = current; p->d.next; p = p->d.next) {
        if (p->d.failed++ > 4) {
            current = p->d.next;
        }
    }

    p->d.next = new;

    pool->current = current ? current : new;

    return m;
}

やはり新しいメモリブロックを作成している。新しいメモリブロックへ切り替える条件(forループの箇所)がいまいちわからないけど、それはまぁ置いておこう。

ngx_palloc_large

最後に、作成したメモリプールよりも大きなサイズを確保しようとした際に呼ばれるngx_palloc_large。

static void *
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
    void              *p;
    ngx_uint_t         n;
    ngx_pool_large_t  *large;

    p = ngx_alloc(size, pool->log);
    if (p == NULL) {
        return NULL;
    }

    n = 0;

    for (large = pool->large; large; large = large->next) {
        if (large->alloc == NULL) {
            large->alloc = p;
            return p;
        }

        if (n++ > 3) {
            break;
        }
    }

    large = ngx_palloc(pool, sizeof(ngx_pool_large_t));
    if (large == NULL) {
        ngx_free(p);
        return NULL;
    }

    large->alloc = p;
    large->next = pool->large;
    pool->large = large;

    return p;
}

う〜ん、作成したメモリプールのブロックよりも大きい領域確保した場合はその都度新規にメモリ確保して別のリストで管理するという感じかな。

core/ngx_palloc.h
struct ngx_pool_large_s {
    ngx_pool_large_t     *next;    /* 次のメモリブロック(large)へのポインタ    */
    void                 *alloc;   /* メモリブロック(large)へのポインタ        */
};

typedef struct {
    u_char               *last;    /* メモリブロックの現在位置                 */
    u_char               *end;     /* メモリブロックの終了位置                 */
    ngx_pool_t           *next;    /* 次のメモリブロックへのポインタ           */
    ngx_uint_t            failed;  /* 利用するメモリブロックの切り替え用フラグ */
} ngx_pool_data_t;

struct ngx_pool_s {
    ngx_pool_data_t       d;       /* 実際のメモリブロック                     */
    size_t                max;     /* メモリプールの最大メモリサイズ           */
    ngx_pool_t           *current; /* 現在利用中のメモリブロックへのポインタ   */
    ngx_chain_t          *chain;   /* まだわからない                           */
    ngx_pool_large_t     *large;   /* メモリプールより大きいメモリブロック     */
    ngx_pool_cleanup_t   *cleanup; /* まだわからない                           */
    ngx_log_t            *log;     /* エラー等の出力先のログ                   */
};

chainとcleanupはまだわからないけど、仕組み自体は大体理解できたと思うのでこれくらいにする。
昔自分が作ったメモリプールの実装とよく似てるけど、若干違ってたりして面白いですね。

次回(予定)

nginxソースコードリーディング その5〜コアAPI(リスト)〜