Tips for Developing Apache 2.0.x modules

[ はじめに | 排他処理 | 共有メモリ | ファイル入出力 | 書式指定 | リクエストの処理 | C++ | デバッグ | 参考文献 ]

はじめに

このページでは,私が Apache 2.x 向けのモジュール開発の過程で得た Tips について紹介しています.

排他処理

他のプロセス及びスレッドに対して排他処理を行うには apr_global_lock_t が,他のスレッドに対して排他処理を行うには apr_thread_mutex_tapr_thread_rwlock_t が利用できます.

apr_global_mutex_t の使い方

基本的には,次の手順で使用します.

  1. post_config ステージ
  2. child_init ステージ
  3. クリティカルセクションの開始
  4. クリティカルセクションの終了

クリティカルセクションの開始/終了の詳細については,上記の関数を呼ぶ だけなので,apr_global_mutex.h のドキュメントを参照してください.関 数の戻り値のチェックだけ気をつけておけば,特につまずく点はないと思い ます.また,apr_global_mutex_t のリソースは Apache の終了時に自動的 に解放されるので,明示的 に apr_global_mutex_destroy を呼ぶ必要はあ りません.

post_config ステージにおい て unixd_set_global_mutex_perms を実行すべ きかどうかは AP_NEED_SET_MUTEX_PERMS が define されているかどうかで判断すれば良いのですが,このマクロは Apache 2.0 系には用意されていません.Apache 2.0 系のモジュールを作成 する場合は次のようにしてモジュール側で define してやる必要があります.

/* Apache 2.0 系用に,AP_NEED_SET_MUTEX_PERMS の define を試みる. */
#ifndef AP_NEED_SET_MUTEX_PERMS
#if !defined(OS2) && !defined(WIN32) && !defined(BEOS) && !defined(NETWARE)
#define AP_NEED_SET_MUTEX_PERMS
#endif
#endif

以上をふまえると,モジュールのコードは次のようになります.

#ifndef AP_SERVER_MAJORVERSION_NUMBER
#if !defined(OS2) && !defined(WIN32) && !defined(BEOS) && !defined(NETWARE)
#define AP_NEED_SET_MUTEX_PERMS
#endif
#endif

#include "apr_global_mutex.h"
#include "apr_strings.h"
#include "stdio.h"
#ifdef AP_NEED_SET_MUTEX_PERMS
#include "unixd.h"
#endif

typedef struct ServerConfig {
    apr_global_mutex_t *glock;
} sconfig;

static void *create_server_config(apr_pool_t *p, server_rec *s)
{
    sconfig *config;

    config = (sconfig *)(apr_palloc(p, sizeof(sconfig)));

    return config;
}

static int post_config(apr_pool_t *pconf, apr_pool_t *plog,
                       apr_pool_t *ptemp, server_rec *s)
{
    sconfig *config;
    void *user_data;
    apr_status_t status;

    /* post_config は Apache の起動時に二度呼ばれるので,ダミーの */
    /* user_data を使って一度目では apr_global_mutex_t を生成しない */
    /* ようにする. */
    apr_pool_userdata_get(&user_data, USER_DATA_KEY, s->process->pool);
    if (user_data == NULL) {
        apr_pool_userdata_set((const void *)(1), USER_DATA_KEY,
                              apr_pool_cleanup_null, s->process->pool);
        return OK;
    }

    do {
        config = (sconfig *)(ap_get_module_config(s->module_config, &sample_module));

        status = apr_global_mutex_create(&(config->glock), NULL,
                                         APR_LOCK_DEFAULT, pconf);
        if (status != APR_SUCCESS) {
            return HTTP_INTERNAL_SERVER_ERROR;
        }
    } while ((s = s->next) != NULL);

#ifdef AP_NEED_SET_MUTEX_PERMS
    status = unixd_set_global_mutex_perms(config->glock);
    if (status != APR_SUCCESS) {
        return HTTP_INTERNAL_SERVER_ERROR;
    }
#endif

    return OK;
}

static void child_init(apr_pool_t *p, server_rec *s)
{
    sconfig *config;

    do {
        config = (sconfig *)(ap_get_module_config(s->module_config, &sample_module));

        if (apr_global_mutex_child_init(&(config->glock), NULL, p) != APR_SUCCESS) {
            SERROR("Can not attach to global mutex.");

            return;
        }
    } while ((s = s->next) != NULL);
}

static void register_hooks(apr_pool_t *pool)
{
    ap_hook_post_config(post_config, NULL, NULL, APR_HOOK_REALLY_FIRST);
    ap_hook_child_init(child_init, NULL, NULL, APR_HOOK_REALLY_FIRST);
    /* (略)*/
}

module AP_MODULE_DECLARE_DATA uploader_module = {
    STANDARD20_MODULE_STUFF,
    NULL,
    NULL,
    create_server_config,
    NULL,
    NULL,
    register_hooks
};

apr_thread_mutex_t の使い方

次のようにして使用します.

  1. 初期化
  2. クリティカルセクションの開始
  3. クリティカルセクションの終了
  4. 後始末

環境によってはスレッドを扱えない場合があるので,マク ロ APR_HAS_THREADS が 0 以外の時だ け apr_thread_mutex_t を使うようにすると良いです.

#include "apr.h"
#if APR_HAS_THREADS
#include "apr_thread_mutex.h"
#endif

/* (略)*/

apr_status_t lock()
{
#if APR_HAS_THREADS
    return apr_thread_mutex_lock(mutex);
#else
    return APR_SUCCESS;
#endif
}

使用に関しては特につまずく点は無いと思います.apr_thread_mutex.h の ドキュメントを参照してください.

apr_thread_rwlock_t の使い方

変更はあまりされないけど,頻繁に参照されるデータの保護に使用します. ロックの方法は増えてますが,基本的な使用方法 は apr_thread_mutex_t と同じです.

共有メモリ

他のプロセスとメモリの共有を行うには apr_shm_t を利用します.実際に 動くコンパクトなサンプルとしては, mod_shm_counter がお薦めです.

apr_shm_t の使い方

基本的には,次の手順で使用します.

  1. post_config ステージ
  2. child_init ステージ

モジュールのコードは次のようになります.

#include "apr_shm.h"
typedef struct ServerConfig {
    char *shm_path;
    /* 共有メモリ */
    apr_shm_t *shm_data;
    /* プロセスが実際に読み書きするメモリ */
    char *data;
} sconfig;

static void *create_server_config(apr_pool_t *p, server_rec *s)
{
    sconfig *config;

    config = (sconfig *)(apr_palloc(p, sizeof(sconfig)));

    config->shm_path = "/path/to/shm/file";
    config->data_shm = NULL;

    return config;
}

static int post_config(apr_pool_t *pconf, apr_pool_t *plog,
                       apr_pool_t *ptemp, server_rec *s)
{
    sconfig *config;
    void *user_data;
    apr_status_t status;

    /* post_config は Apache の起動時に二度呼ばれるので,ダミーの */
    /* user_data を使って一度目では apr_shm_t を生成しない */
    /* ようにする. */
    apr_pool_userdata_get(&user_data, USER_DATA_KEY, s->process->pool);
    if (user_data == NULL) {
        apr_pool_userdata_set((const void *)(1), USER_DATA_KEY,
                              apr_pool_cleanup_null, s->process->pool);
        return OK;
    }

    do {
        config = (sconfig *)(ap_get_module_config(s->module_config, &sample_module));

        /* 1024 バイトの共有メモリを設定 */
        status = apr_shm_create(&(config->shm_data), sizeof(char)*1024,
                                config->shm_path, pconf);
        if (status != APR_SUCCESS) {
            return HTTP_INTERNAL_SERVER_ERROR;
        }

        config->data = (char *)(apr_shm_baseaddr_get(config->shm_data);

        /* 必要に応じて初期化 */
        memset(config->shm_data, 0, sizeof(char)*1024);
    } while ((s = s->next) != NULL);

    return OK;
}

static void child_init(apr_pool_t *p, server_rec *s)
{
    sconfig *config;

    do {
        config = (sconfig *)(ap_get_module_config(s->module_config, &sample_module));

        /* この条件が成り立つときのみ attach する. */
        if (!config->data_shm) {
            if (apr_shm_attach(&(config->data_shm), config->shm_path,
                               p) != APR_SUCCESS) {
                return;
            }
        }

        config->data = (char *)(apr_shm_baseaddr_get(config->data_shm));
    } while ((s = s->next) != NULL);
}

ファイル入出力

ファイル入出力がらみの注意点について説明します.

バッファリング

APR は,デフォルトでは apr_file_write, apr_file_read 等での入出力の際にバッファリン グを行いません.次のように,apr_file_open の際に, APR_BUFFERED フラグを立ててやることでバッファ リングするようになります.

apr_file_t *file;
const char *file_path;
apr_pool_t *pool;

apr_file_open(&file, file_name, APR_READ|APR_BUFFERED, APR_OS_DEFAULT, pool);

mmap

mmap を使ってファイルへの書き込みを行う場合, apr_mmap_create のフラグに は APR_MMAP_WRITE だけでな く APR_MMAP_READ を指定する必要があります. APR_MMAP_WRITE だけの場合,一部の環境で正常 に動作しません.

また,apr_file_open の際に APR_BUFFERED フラグを立ててた開いたファイル は mmap を使って読み書きできません.apr_mmap_create を呼ぶ と APR_EBADF が返されて失敗します.

書式指定

Apache および APR には,stdio.h や stdarg.h の *printf 関数の代わり に使える次の関数が用意されています.

これらに渡す書式指定 format には,通常の *printf 関数と同じものが使 えますが,数値に関しては注意が必要です.通常,数値に対しては「%d」や 「%lu」等の変換指定を用いますが,多くのアーキテクチャで動作させるこ とを考えた場合,これはあまり望ましくありません.あるアーキテクチャで 正常に動いていても別のアーキテクチャでは,違った値が出力されなかった り,不正なメモリアクセスが発生したりします. APR では,この問題を解決するために apr.h で次のような変換指定文字マ クロが定義されています.

これらを用いることで,特定のアーキテクチャに依存しない安全なコードに することができます. たとえば, apr_size_tapr_off_tapt_time_t を出力する際は,次のようにし ます.

apr_size_t size;
apr_off_t offset;
apr_time_t time;
const char *str;

/* (略)*/

str = apr_psprintf(pool,
                   "size = %" APR_SIZE_T_FMT ", "
                   "offset = %" APR_OFF_T_FMT ", "
                   "time = %" APR_UINT64_T_FMT,
                   size, offset, time);

リクエストの処理

Apache 2.x のコンテンツハンドラで POST されたデータを読み込むには次 の二つの方法があります.

原因は調べ切れていないのですが,後者の方法を使った場合,POST された データのサイズに比例した(大体 1/40)のメモリをApache 内部で消費してし まうようです.巨大なデータを扱うモジュールを作成する場合は注意してく ださい.

C++

Apche のモジュールを C++ で開発する場合,いくつか注意点があります.基本的な物については, Apache API C++ Cookbook を参照してください.Apache 1.x 向けに書かれた物ですが, ほとんどの項目はそのまま Apache 2.x にも使えます. 以下では,その他の注意点について説明します.

APR_INT64_C

C++ でモジュールを書いていて,「INT64_C が未定義です.」みたいなエラー が出た場合は,マク ロ __STDC_CONSTANT_MACROS を define してや りましょう.(追記: apr.h において define するようになっているので,apr.h を最初に include するようにしても OK)

参考: /usr/include/stdint.h

/* The ISO C99 standard specifies that in C++ implementations these
   should only be defined if explicitly requested.  */
#if !defined __cplusplus || defined __STDC_CONSTANT_MACROS
... (INT64_C の define)
#endif

デバッグ

Apache のモジュールを開発する際の便利なデバッグ方法を紹介します.

Apache の Debug ビルド

モジュールを開発する場合,APR Pool のデバッグ機能を有効にした Apache を用いることで,メモリ関連のバグを早い段階で見つけることがで きます.Unix の場合は次のようにすることで Debug ビルドが行えます. (参考: APR_POOL_DEBUG is functioning again WAS: RE: Don't use APR_POOL_DEBUG )

$ env CPPFLAGS=-DAPR_POOL_DEBUG ./configure ...
$ make

Windows の場合は,次のようにします. (参考: Compiling Apache for Microsoft Windows )

> vsvar32.bat
> nmake /f Makefile.win _apached

UNIX でのデバッグ

メモリ関連のバグの場合,デバッグオプション(-g) をつけてコンパイルし, Valgrind 上で Apache を実行するのが手軽です.Apache にデバッグモード (-X) を指定するのがポイントです.

$ valgrind -v --show-reachable=yes --tool=memcheck /usr/sbin/apache2 -X -f /path/to/httpd.conf 2>&1 | tee valgrind.log

Apache をデバッグモードで実行した場合,標準出力への出力がコンソール に表示されるので,いつもの(?) printf デバッグも行えます.

Windows でのデバッグ

普通の Windows アプリケーションの場合と同様,デバッグオプション(/Od /MDd /GS /RTCsu /Zi) をつけてコンパイルして,VisualStudio を使うのが お薦めです.

参考文献

モジュールの開発に役立つ文献を紹介します.

The Apache Modules Book Apacheモジュール プログラミングガイド Secure Coding in C And C++

The Apache Modules Book: Application Development With Apache
Apache 2.x のモジュール作成に必要になるのモジュール作成に必要になる 事柄を一通り説明した本.トップダウンで全体を眺めた後,ボトムアップ で詳しい説明がされているので非常に理解しやすいです.
Apacheモジュール プログラミングガイド
Apache のモジュール作成に必要になる事柄を一通り説明した本です.痒い ところに手が届いているので,手元に置いておくと重宝します.
Advanced Topics in Module Design: Threadsafety and Portability
Apache2 でモジュールを作成するときに必要になってくるテクニックが解 説されたパワポです.そんなに長くないので,モジュール書く前にさらっと読 んでおきましょう.
Apache API C++ Cookbook
C++ を使って Apache のモジュールを作成する際の注意事項について説明 したサイトです.
libapr(apache portable runtime) programming tutorial
APR のチュートリアルです.サンプルコードおよび,間違いやすい点につ いての記述が多いので重宝します.
Secure Coding in C And C++
C/C++ でプログラムを作るときにセキュリティホールが発生してしまう原 理とその対策について詳しく説明した本です.丁寧に書かれているので内 容をしっかりと理解することができます.