ライブラリで使用するスタックサイズを求めた結果、常にこれ!ってのは決められないなってなったので記事にしました。
スタックサイズを絞りたい
###スタック領域とは?
メモリ領域にはヒープ領域とスタック領域という2種類の領域があり、スタック領域の方は例えば以下の情報が詰められます。(全てかは未確認。)
- 実行される関数の実体・引数
- 関数・メインで使用されるローカル変数
これらは確保できないとプログラムが動作しなくなるため、しっかりサイズを確保しないといけない大事なものになります。
たまにメモリ破壊系の不具合で見かけるスタックオーバーフローはこのスタックサイズ以上のスタックを利用としているからですね。
以下サイトがわかりやすいです。
https://www.uquest.co.jp/embedded/learning/lecture07-2.html
###なぜサイズを絞りたいのか?
上記のように大事な大事なスタックサイズです。さてLinuxでthreadを立ち上げた場合はどのくらい必要なんでしょう。
以前にスタックサイズとか気にせずにメモリの変化を確認したところ、1 threadで8MByte消費していました。
環境はUbuntu18.04 64bitです。man page曰く32bitでもデフォルトは2MByte。スレッド立ち上げでガリガリメモリが削られるのが目に見えます。
今回サイズを求めたいのは私が作ってるthreadpoolのライブラリなんですが、デフォルトのままでは組み込み分野では使えない。Linux Cなのに組み込みでは使えないなんて面白くない(というかメインターゲットが組み込み系です)。
なのでいい感じに設定してあげないと。
というわけでサイズを絞ろうというのが目的でした。
使用スタックサイズを求める
その1: gccの機能を活用する。
記事作成後にいただいた@tenmyo さんのコメントを試してみました。やり方は簡単。gccのオプションに-fstack-usageを付けるだけ。
するとプログラムを実行すれば、各コードの配下に.suというファイルが出力され、関数の使用スタックサイズが表示されます。
こんな感じに。関数と使用スタックが表示されます。
$ cat threadpool/lib/event_thread.su
event_thread.c:430:13:event_tpool_thread_call_msgs 208 dynamic,bounded
event_thread.c:445:13:event_tpool_thread_cb 48 dynamic,bounded
event_thread.c:424:13:event_tpool_thread_msg_cb_stop 8 static
event_thread.c:289:13:event_subscriber_data_free.isra.2 16 static
event_thread.c:411:13:event_tpool_thread_msg_cb_del 32 static
event_thread.c:397:13:event_tpool_thread_msg_cb_update 64 dynamic,bounded
event_thread.c:383:13:event_tpool_thread_msg_cb_add 80 dynamic,bounded
event_thread.c:164:13:event_thread_msg_send.isra.7 224 dynamic,bounded
event_thread.c:190:13:event_thread_msg_send_subscribe 64 static
event_thread.c:360:15:event_tpool_thread_main 48 dynamic,bounded
event_thread.c:461:18:event_tpool_thread_new 80 dynamic,bounded
event_thread.c:493:6:event_tpool_thread_start 96 static
event_thread.c:509:6:event_tpool_thread_stop 48 dynamic,bounded
event_thread.c:525:6:event_tpool_thread_add 48 dynamic,bounded
event_thread.c:528:6:event_tpool_thread_update 48 dynamic,bounded
event_thread.c:533:6:event_tpool_thread_del 48 dynamic,bounded
event_thread.c:536:6:event_thread_atfork_child 16 static
event_thread.c:543:6:event_thread_set_stack_size 8 static
トータルがサイズがわかるような簡易スクリプトも作ってみました。
LIST=`find $1 -name *.su | xargs cat | awk -F" " '{print $2}'`
data=0
for value in $LIST; do data=`expr $data + $value`; done
echo "Stack size under $1: $data byte"
出力はこんな感じ。
$ ./threadpool/test.sh threadpool
Stack size under threadpool: 141328 byte
オプションで1発とは凄いですねgcc!
その2: threadのスタックアドレスを設定する
こちらのサイトを参考に、最大スタックサイズを確保したアドレスを利用して、joinする時に結果を表示するようにしました。
http://oswald.hatenablog.com/entry/20101205/1291518843
私の場合は求めたいのがthreadpoolライブラリのスレッドで使用するスタックサイズなので、threadを管理するEventTPoolThreadクラスstack_adrを追加
/*! thread information */
struct event_tpool_thread_t {
EventSubscriberData head;/*!<list of subscriber*/
EventSubscriberData tail;/*!<list of subscriber*/
event_thread_msg_info_t msgdata;/*<! message queue*/
int eventfd;/*!<eventfd to use add message*/
EventInstance event_base;/*<! base event*/
EventHandler msg_evinfo;/*<! msg event info*/
pthread_t tid;/*<! thread id*/
int is_stop;/*<! stop flag*/
#ifdef CHECK_STACKSIZE
char * stack_adr;
#endif
};
pthread_create時に初期化マジックナンバーで初期化した上でpthread_attr_setstack
で設定
/** start thread */
void event_tpool_thread_start(EventTPoolThread this) {
this->tid=0;
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, EVENT_THREAD_STACKSIZE);
#ifdef CHECK_STACKSIZE
this->stack_adr = (char *) malloc(EVENT_THREAD_STACKSIZE);
memset(this->stack_adr, MAGIC_NUMBER, EVENT_THREAD_STACKSIZE);
pthread_attr_setstack(&attr, (void *) this->stack_adr, EVENT_THREAD_STACKSIZE);
#endif
pthread_create(&this->tid, &attr, event_tpool_thread_main, this);
pthread_attr_destroy(&attr);
}
Join後に結果を表示。スタックで利用されたアドレスがわかるという寸法です。
/** stop thread */
void event_tpool_thread_stop(EventTPoolThread this) {
pthread_t tid = this->tid;
int ret = event_thread_msg_send_stop(this);
if(0<ret) {
pthread_join(tid, NULL);
}
#ifdef CHECK_STACKSIZE
int i=0;
for(i=0;i<EVENT_THREAD_STACKSIZE;i++) {
if (this->stack_adr[i] != MAGIC_NUMBER) break;
}
fprintf(stderr, "Used %d byte\n", EVENT_THREAD_STACKSIZE - i);
#endif
}
結果
テストコードを実行してみたところ、テストケース1では16264 byte、テストケース2では21384byteと、動作ルート毎にサイズが変わっています。
なんでかなと思いつつサイズを絞ってlighttpdにぶち込んだところスタックオーバーフロー発生。原因は私がいじったスタックを気にしないcgiの実装のせいでした。
ここでふと考える。なんでライブラリ内の関数と関係ない所のスタックサイズが影響するのか?
答えは簡単です。関数ポインタを使ってライブラリに関数登録・実行してるから。
自分でも実行される関数でスタックを使うとかいてるじゃないか、気付きなさいよ!
スタックは関数実行時に積まれるので、関数ポインタだろうが実体だろうが同じことですよね。
結論・関数ポインタを利用するライブラリ、スタックサイズなんて決められない!
あくまでそのライブラリ使用ケースも合わせてスタックサイズ計算ならできますが、このケースではライブラリ単品でのスタックサイズ決定なんてできません。だって関数実体はライブラリ使用者に用意してもらうんですから。
というわけで、サイズが決められないなら設定できるようにしよう。pthread仕様のように。という結論に落ち着きました。
計測してよかった(笑)
参考
スタックについてわかりやすい解説をしてくれているサイト。その他下回りにも近い知識をわかりやすく説明してくれています。
スタックってなあに?(2)
関数ポインタだろうがスタック使うことに気付かせてくれたサイト
「関数ポインタ」と「関数テーブル」
スタックサイズを求める方法の参考
関数のスタック使用量を計算する方法