Apache MPM worker の pmap が分からんという話から Linux pthread (NPTL) のスタックサイズとガード領域まで

ひょんなことから 32bit カーネルで動いている Apache (MPMは worker = マルチスレッド) を /usr/bin/pmap で調べていました。調べている中で pthread のスタックの割当テ方にも興味がわいたので glibc なども深追いして調べました。

以降の内容は次の環境で調べた内容です。

pmap で [anon] が一杯でてくる

worker プロセスの pmapを取ると一定サイズの [anon] が連続して出てくるのはなんだったかな ? と。

  • /usr/bin/pmap の出力
# 説明のために pmap の出力を一部省略

b23de000      4K -----    [ anon ]
b23df000  10240K rw---    [ anon ]
b2ddf000      4K -----    [ anon ]
b2de0000  10240K rw---    [ anon ]
b37e0000      4K -----    [ anon ]
b37e1000  10240K rw---    [ anon ]
b41e1000      4K -----    [ anon ]
b41e2000  10240K rw---    [ anon ]
b4be2000      4K -----    [ anon ]
b4be3000  10240K rw---    [ anon ]
b55e3000      4K -----    [ anon ]
b55e4000  10240K rw---    [ anon ]
b5fe4000      4K -----    [ anon ]
b5fe5000  10240K rw---    [ anon ]
b69e5000      4K -----    [ anon ]
b69e6000  10240K rw---    [ anon ]
b73e6000      4K -----    [ anon ]
b73e7000  10240K rw---    [ anon ]
b7de7000    244K rw-s-  /dev/zero (deleted)
b7e24000    504K rw-s-  /dev/zero (deleted)
b7ea2000    520K rw---    [ anon ]
b7f24000    100K rw-s-  /dev/zero (deleted)
b7f3d000     28K rw---    [ anon ]
bf865000     84K rw---    [ stack ]

10240KB … 4kB ... 10240KB ... 4KB の [anon] (Anoymous memory region = 無名メモリリージョン) が連なっています

スレッドのスタックだった

http://mail-archives.apache.org/mod_mbox/httpd-dev/200710.mbox/%3C471B3E62.7050609@mcarlson.com%3E

上記MLで Apache の ThreadStackSize 設定を変更した pmap の出力が投稿されています。これから 10240KB [anon] のリージョンはスレッドごとに用意されたスタックのメモリリージョンだと分かりました。Apache の MPM が worker だとスレッドを生やしますよね。

ThreadStackSize を 変更してみる

スタックのサイズは httpd.conf で ThreadStackSize を指定することで変更できます。適当に 2MBにしてみます。

  • /etc/httpd/conf/httpd.conf
# スタックのサイズは bytes 指定
ThreadStackSize  2097152 
  • /usr/bin/pmap の出力
# 説明のために pmap の出力を一部省略

b6e8f000      4K -----    [ anon ]
b6e90000   2048K rw---    [ anon ]
b7090000      4K -----    [ anon ]
b7091000   2048K rw---    [ anon ]
b7291000      4K -----    [ anon ]
b7292000   2048K rw---    [ anon ]
b7492000      4K -----    [ anon ]
b7493000   2048K rw---    [ anon ]
b7693000      4K -----    [ anon ]
b7694000   2048K rw---    [ anon ]
b7894000      4K -----    [ anon ]
b7895000   2048K rw---    [ anon ]
b7a95000      4K -----    [ anon ]
b7a96000   2048K rw---    [ anon ]
b7c96000      4K -----    [ anon ]
b7c97000   2048K rw---    [ anon ]
b7e97000    244K rw-s-  /dev/zero (deleted)
b7ed4000    504K rw-s-  /dev/zero (deleted)
b7f52000    520K rw---    [ anon ]
b7fd4000    100K rw-s-  /dev/zero (deleted)
b7fed000     28K rw---    [ anon ]
bfcc2000     84K rw---    [ stack ]

10MB (10240 KB) から -> 2M(2048 KB) に変わったのが確認できました。

ここからは pthread の話

Apacheの話は一旦休憩。

スタックのサイズを変更する実装がどうなっているか気になったので pthread 周りをもう深追いしました。Linux の場合は、Apache が利用しているスレッド実装は NPTL = Native POSIX Thread Library でいいですよね

$ getconf GNU_LIBPTHREAD_VERSION
NPTL 2.5

# http://orz.makegumi.jp/archives/1258430.html でコマンドを知りました

( LinuxThreads など他の実装についてはちょっと分かりません :q )

謎のリージョン

先ほどの pmap で表示される 10240KB ... 4KB ... 10240KB ... 4KB ... という並びの 4KB の部分も気になったので、役割を調べました

b572b000       4       -       -       - -----    [ anon ]    # こいつ
b572c000   10240       -       -       - rw---    [ anon ]


pthread_attr_getguardsize(3) に解説がのっていました。ガード領域 ( guard area ) と呼ぶようです

ガード領域は、読み出し/書き込みアクセスが行われないように保護がかけ られた仮想メモリページで構成で構成される。スレッドがスタックをガード 領域までオーバーフローさせた場合、ほとんどのハードウェアアーキテクチャ では、スレッドに SIGSEGV シグナルが送られ、オーバーフローが発生した ことが通知される。


スタックのメモリリージョンの属性に rw (読み書き) がついているのに対して、ガード領域は属性がついていません。プロセスがガード領域のアドレスにアクセスすると、カーネルが以下の様に処理するはずです

  • 1. ガード領域のメモリリージョンにはページフレームが割り当てられていない
  • 2. プロセスがアクセスするとページフォルトが発生
  • 3. 読み/書き/実行が許可されないリージョンでのページフォルトなのでセグメンテーションフォルトとして扱われる
  • 4. プロセスに SIGSEGV が飛ぶ

こうしてスタックオーバーフローのガード として働くのですね。(詳しいことは『詳解Linuxカーネル』などをあたってください )

pthread で スタックのサイズとガード領域のサイズを変える

スタックのサイズとガード領域のサイズを変更したい場合は、pthread の pthread_attr_setstacksize(3), pthread_attr_setguardsize(3) を使って変更できることが分かりました。

動作を確かめるために、下記の様なコードでスタックサイズとガード領域のサイズを変更しながら pmap をいろいろ取ってみました。 (64bit カーネルのプロセスのメモリレイアウトがよく知らないので32bit カーネルで試しています)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <err.h>
#include <errno.h>
#include <limits.h>
#include <pthread.h>

#define NUM_THREADS 10

void * do_nothing(void *arg) {
    pause();
    return NULL;
}

int main(int argc, char *argv[]) {

    if (argc != 3) {
        errx(1, "usage: %s <page frames for stack> <page frames for guard>", argv[0]);
    }

    size_t stack_size = sysconf(_SC_PAGESIZE) * atoi(argv[1]);
    size_t guard_size = sysconf(_SC_PAGESIZE) * atoi(argv[2]);

    /**
     * スタックサイズが PTHREAD_STACK_MIN より小さいと
     * pthread_attr_setstacksize が EINVAL - invalid argument を返す
     **/
    if (stack_size < PTHREAD_STACK_MIN) {
        errx(1, "stack size must be greater than PTHREAD_STACK_MIN (%d bytes)", PTHREAD_STACK_MIN);
    }

    pthread_attr_t attribute;
    int status = pthread_attr_init(&attribute);
    if(status != 0) {
        errx(1, "pthread_attr_init: %s", strerror(status));
    }

    status = pthread_attr_setstacksize(&attribute, stack_size);
    if(status != 0) {
        errx(1, "pthread_attr_setstacksize: %s", strerror(status));
    }

    status = pthread_attr_setguardsize(&attribute, guard_size);
    if(status != 0) {
        errx(1, "pthread_attr_setguardsize: %s", strerror(status));
    }

    status = pthread_attr_getstacksize(&attribute, &stack_size);
    if(status != 0) {
        errx(1, "pthread_attr_getstacksize: %s", strerror(status));
    }

    status = pthread_attr_getguardsize(&attribute, &guard_size);
    if(status != 0) {
        errx(1, "pthread_attr_getguardsize: %s", strerror(status));
    }

    printf("stack_size: %zd\nguard_size: %zd\n", stack_size, guard_size);

    pthread_t thread[NUM_THREADS];
    for (int i = 0; i < NUM_THREADS; i++) {
        status = pthread_create(&thread[i], &attribute, do_nothing, NULL);
        if(status != 0) {
            errx(1, "pthread_create: %s",strerror(status));
        }
    }

    char shell_pmap[128];
    snprintf(shell_pmap, sizeof(shell_pmap), "/usr/bin/pmap %d", getpid());
    system(shell_pmap);

    exit(0);
}
$ gcc pmap_of_pthread.c -o pmap_of_pthraed -pthread -std=gnu99 -W -Wall -Wno-unused-parameter
  • ページフレーム数でスタックとガード領域のサイズを指定します
[usage] ./pmap_of_pthraed <page frames for stack> <page frames for guard>

なおスレッドのスタックは PTHREAD_STACK_MIN より大きいサイズを指定する必要があります。それより小さい場合は EINVAL を返します。

スタックサイズを 100KB , ガード領域を 4KB にした場合

$ ./pmap_of_pthraed 25 1 
stack_size: 102400
guard_size: 4096
4442:   ./pmap_of_pthraed 25 1
00224000    108K r-x--  /lib/ld-2.5.so
0023f000      4K r-x--  /lib/ld-2.5.so
00240000      4K rwx--  /lib/ld-2.5.so
006b8000   1372K r-x--  /lib/libc-2.5.so
0080f000      8K r-x--  /lib/libc-2.5.so
00811000      4K rwx--  /lib/libc-2.5.so
00812000     12K rwx--    [ anon ]
008b0000      4K r-x--    [ anon ]
00d37000     88K r-x--  /lib/libpthread-2.5.so
00d4d000      4K r-x--  /lib/libpthread-2.5.so
00d4e000      4K rwx--  /lib/libpthread-2.5.so
00d4f000      8K rwx--    [ anon ]
08048000      4K r-x--  /root/pmap_of_pthraed
08049000      4K rw---  /root/pmap_of_pthraed
08491000    132K rw---    [ anon ]
b7e93000      4K -----    [ anon ]
b7e94000     96K rw---    [ anon ]
b7eac000      4K -----    [ anon ]
b7ead000     96K rw---    [ anon ]
b7ec5000      4K -----    [ anon ]
b7ec6000     96K rw---    [ anon ]
b7ede000      4K -----    [ anon ]
b7edf000     96K rw---    [ anon ]
b7ef7000      4K -----    [ anon ]
b7ef8000     96K rw---    [ anon ]
b7f10000      4K -----    [ anon ]
b7f11000     96K rw---    [ anon ]
b7f29000      4K -----    [ anon ]
b7f2a000     96K rw---    [ anon ]
b7f42000      4K -----    [ anon ]
b7f43000     96K rw---    [ anon ]
b7f5b000      4K -----    [ anon ]    # ガード
b7f5c000     96K rw---    [ anon ]    # スタック 96KB ?
b7f74000      4K -----    [ anon ]    # ガード
b7f75000    104K rw---    [ anon ] 
b7f96000      4K rw---    [ anon ]
bfd7e000     84K rw---    [ stack ]
 total     2856K

スタックのサイズを 100KB と指定しているのに pmap を取ると 96K になっちゃってます。間違ったコードを書いたのかと思ったらどうやら glibc の実装バグがあるようです

バグ

glibc 2.8 の時点では、 NPTL スレッド実装ではガード領域はスタックサイズ で割り当てられる領域の中に含まれている。一方、POSIX.1 では、スタックの 末尾に追加の領域を割り当てることが求められている

Man page of PTHREAD_ATTR_SETSTACKSIZE より引用
とあるのですが SL6 glibc-2.12-1 で試してみても同じレイアウトになりました。分からない

スタックサイズを 100KB , ガード領域を 8KB にした場合

 ./pmap_of_pthraed 25 2
stack_size: 102400
guard_size: 8192
4454:   ./pmap_of_pthraed 25 2
00264000   1372K r-x--  /lib/libc-2.5.so
003bb000      8K r-x--  /lib/libc-2.5.so
003bd000      4K rwx--  /lib/libc-2.5.so
003be000     12K rwx--    [ anon ]
00513000    108K r-x--  /lib/ld-2.5.so
0052e000      4K r-x--  /lib/ld-2.5.so
0052f000      4K rwx--  /lib/ld-2.5.so
00675000      4K r-x--    [ anon ]
0087c000     88K r-x--  /lib/libpthread-2.5.so
00892000      4K r-x--  /lib/libpthread-2.5.so
00893000      4K rwx--  /lib/libpthread-2.5.so
00894000      8K rwx--    [ anon ]
08048000      4K r-x--  /root/pmap_of_pthraed
08049000      4K rw---  /root/pmap_of_pthraed
08a21000    132K rw---    [ anon ]
b7eb1000      8K -----    [ anon ]
b7eb3000     92K rw---    [ anon ]
b7eca000      8K -----    [ anon ]
b7ecc000     92K rw---    [ anon ]
b7ee3000      8K -----    [ anon ]
b7ee5000     92K rw---    [ anon ]
b7efc000      8K -----    [ anon ]
b7efe000     92K rw---    [ anon ]
b7f15000      8K -----    [ anon ]
b7f17000     92K rw---    [ anon ]
b7f2e000      8K -----    [ anon ]
b7f30000     92K rw---    [ anon ]
b7f47000      8K -----    [ anon ]
b7f49000     92K rw---    [ anon ]
b7f60000      8K -----    [ anon ]
b7f62000     92K rw---    [ anon ]
b7f79000      8K -----    [ anon ]
b7f7b000     92K rw---    [ anon ]
b7f92000      8K -----    [ anon ]
b7f94000    100K rw---    [ anon ]
b7fb4000      4K rw---    [ anon ]
bf94f000     84K rw---    [ stack ]
 total     2856K

スタックサイズが 92KB, ガード領域が 8KB になっていますね。

スタックサイズを 100KB , ガード領域を 0KB にした場合

./pmap_of_pthraed 25 0
stack_size: 102400
guard_size: 0
4468:   ./pmap_of_pthraed 25 0
00157000   1372K r-x--  /lib/libc-2.5.so
002ae000      8K r-x--  /lib/libc-2.5.so
002b0000      4K rwx--  /lib/libc-2.5.so
002b1000     12K rwx--    [ anon ]
002d0000    108K r-x--  /lib/ld-2.5.so
002eb000      4K r-x--  /lib/ld-2.5.so
002ec000      4K rwx--  /lib/ld-2.5.so
005f8000      4K r-x--    [ anon ]
00cb6000     88K r-x--  /lib/libpthread-2.5.so
00ccc000      4K r-x--  /lib/libpthread-2.5.so
00ccd000      4K rwx--  /lib/libpthread-2.5.so
00cce000      8K rwx--    [ anon ]
08048000      4K r-x--  /root/pmap_of_pthraed
08049000      4K rw---  /root/pmap_of_pthraed
09996000    132K rw---    [ anon ]
b7ef2000   1008K rw---    [ anon ]   # 全体がスタック
b7ff5000      4K rw---    [ anon ]
bfb8d000     84K rw---    [ stack ]
 total     2856K

ガード領域がない場合は、スタックのリージョンが1個だけ確保されるようになりました。

ガード領域はどのように作成されるのか

ガード領域がどのように確保されているのか glibc のソースを見てみました。

  • glibc-2.5/nptl/allocatestack.c
static int
allocate_stack (const struct pthread_attr *attr, struct pthread **pdp,
                ALLOCATE_STACK_PARMS)
{

          /* 略 */

          mem = mmap (NULL, size, prot,
                      MAP_PRIVATE | MAP_ANONYMOUS | ARCH_MAP_FLAGS, -1, 0);

          /* 略 */

          if (mprotect (guard, guardsize, PROT_NONE) != 0)
            {   

mmap(2) で確保したリージョンを mprotect(2) で PROT_NONE 属性を指定する事で 読み/書き/実行不可能なリージョン = ガード領域とするようです。ガード領域はカーネルが管理するものではなくて、スレッドライブラリが自前で用意して管理するものと分かりました。

Apache の話に戻ります

最後に Apache の ThreadStackSize の設定と pthread API を繋げている部分のソースをあげておきます

  • httpd-2.2.24/server/core.c
#ifdef AP_MPM_WANT_SET_STACKSIZE
AP_INIT_TAKE1("ThreadStackSize", ap_mpm_set_thread_stacksize, NULL, RSRC_CONF,
              "Size in bytes of stack used by threads handling client connections"),
#endif
  • httpd-2.2.24/server/mpm_common.c
apr_size_t ap_thread_stacksize = 0; /* use system default */

const char *ap_mpm_set_thread_stacksize(cmd_parms *cmd, void *dummy,
                                        const char *arg)
{
    long value;
    const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY);
    if (err != NULL) {
        return err;
    }

    value = strtol(arg, NULL, 0);
    if (value < 0 || errno == ERANGE)
        return apr_pstrcat(cmd->pool, "Invalid ThreadStackSize value: ",
                           arg, NULL);

    ap_thread_stacksize = (apr_size_t)value;

    return NULL;
}

これは設定ファイルから値読み取って内部の形式に変換する部分。

  • httpd-2.2.24/srclib/apr/threadproc/unix/thread.c
APR_DECLARE(apr_status_t) apr_threadattr_stacksize_set(apr_threadattr_t *attr,
                                                       apr_size_t stacksize)
{
    int stat;

    stat = pthread_attr_setstacksize(&attr->attr, stacksize);
    if (stat == 0) {
        return APR_SUCCESS;
    }
#ifdef HAVE_ZOS_PTHREADS
    stat = errno;
#endif

    return stat;
}

APR_DECLARE(apr_status_t) apr_threadattr_guardsize_set(apr_threadattr_t *attr,
                                                       apr_size_t size)
{
#ifdef HAVE_PTHREAD_ATTR_SETGUARDSIZE
    apr_status_t rv;

    rv = pthread_attr_setguardsize(&attr->attr, size);
    if (rv == 0) {
        return APR_SUCCESS;
    }
#ifdef HAVE_ZOS_PTHREADS
    rv = errno;
#endif
    return rv;
#else
    return APR_ENOTIMPL;
#endif
}

pthread_attr_setstacksize, pthread_attr_setguardsize をただラップしている実装でした。ここまで読んだら調べたことが全部繋がってすっきりです。

まとめ

スレッドに関係する詳しい挙動を追うには低いレイヤのライブラリやシステムコールやカーネルの理解も必要でLL脳にはだいぶ大変ですね。