ひょんなことから 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脳にはだいぶ大変ですね。