UNIXシステムでは、malloc()
が非再入可能関数(システムコール)であることがわかっています。何故ですか?
同様に、printf()
も非再入可能と呼ばれます。どうして?
私は再入可能性の定義を知っていますが、それがこれらの関数に適用される理由を知りたいと思いました。それらが再入可能であることが保証されないのは何ですか?
malloc
およびprintf
は通常、グローバル構造を使用し、内部でロックベースの同期を採用します。そのため、それらは再入可能ではありません。
malloc
関数は、スレッドセーフまたはスレッドセーフでない可能性があります。どちらも再入可能ではありません。
Mallocはグローバルヒープで動作し、同時に発生するmalloc
の2つの異なる呼び出しが同じメモリブロックを返す可能性があります。 (2番目のmalloc呼び出しは、チャンクのアドレスがフェッチされる前に行われる必要がありますが、チャンクは使用不可としてマークされていません)。これはmalloc
の事後条件に違反しているため、この実装は再入できません。
この影響を防ぐために、malloc
のスレッドセーフな実装はロックベースの同期を使用します。ただし、mallocがシグナルハンドラーから呼び出された場合、次の状況が発生する可能性があります。
malloc(); //initial call
lock(memory_lock); //acquire lock inside malloc implementation
signal_handler(); //interrupt and process signal
malloc(); //call malloc() inside signal handler
lock(memory_lock); //try to acquire lock in malloc implementation
// DEADLOCK! We wait for release of memory_lock, but
// it won't be released because the original malloc call is interrupted
この状況は、malloc
が単に別のスレッドから呼び出された場合には発生しません。実際、再入可能性の概念はスレッドセーフを超えており、適切に機能する関数も必要ですその呼び出しの1つが決して終了しない場合でも。これが基本的に、ロック付きの関数が再入できない理由です。
printf
関数もグローバルデータを操作しました。すべての出力ストリームは通常、リソースデータ(端末用またはファイル用のバッファー)に送信されるリソースに接続されたグローバルバッファーを使用します。印刷プロセスは通常、データをバッファにコピーし、その後バッファをフラッシュするシーケンスです。このバッファは、malloc
と同じ方法でロックによって保護する必要があります。したがって、printf
も再入不可です。
リエントラント の意味を理解しましょう。再入可能関数は、前の呼び出しが完了する前に呼び出すことができます。これは、
mallocは空きメモリブロックを追跡するいくつかのグローバルデータ構造を管理しているため、再入可能ではありません。
printfはグローバル変数、つまりFILE * stoutの内容を変更するため、再入可能ではありません。
ここには少なくとも3つの概念があり、それらすべてが口語的な言語で混同されているため、混乱していたのかもしれません。
最も簡単な方法を最初に使用するには:_malloc
とprintf
の両方が thread-safe。それらは、2011年以降、2001年以降のPOSIX、および実際にはずっと以前から、標準Cでスレッドセーフであることが保証されています。これは、次のプログラムがクラッシュしたり、不正な動作をしたりしないことが保証されていることを意味しています。
_#include <pthread.h>
#include <stdio.h>
void *printme(void *msg) {
while (1)
printf("%s\r", (char*)msg);
}
int main() {
pthread_t thr;
pthread_create(&thr, NULL, printme, "hello");
pthread_create(&thr, NULL, printme, "goodbye");
pthread_join(thr, NULL);
}
_
not thread-safeである関数の例はstrtok
です。 2つの異なるスレッドからstrtok
を同時に呼び出すと、結果は未定義の動作になります。strtok
は内部的に静的バッファーを使用してその状態を追跡するためです。 glibcはこの問題を修正するために_strtok_r
_を追加し、C11は_strtok_s
_と同じもの(ただし、ここでは発明されていないため、オプションで別の名前で)を追加しました。
わかりましたが、出力を構築するためにprintf
もグローバルリソースを使用しませんか?実際、2つのスレッドからstdoutに出力するmeanとはどういうことでしょうか同時に?これで次のトピックに進みます。明らかにprintf
は、それを使用するすべてのプログラムで クリティカルセクション になります。クリティカルセクション内に同時に入れることができる実行スレッドは1つだけです。
少なくともPOSIX準拠のシステムでは、これはprintf
をflockfile(stdout)
の呼び出しで開始し、funlockfile(stdout)
の呼び出しで終了することで実現されます。 stdoutに関連付けられたグローバルmutex。
ただし、プログラム内の個別のFILE
は、独自のミューテックスを持つことができます。つまり、1つのスレッドがfprintf(f1,...)
を呼び出すと同時に、2番目のスレッドがfprintf(f2,...)
を呼び出している最中である可能性があります。ここには競合状態はありません。 (libcが実際にこれらの2つの呼び出しを並行して実行するかどうかは QoI の問題です。glibcの機能は実際にはわかりません。)
同様に、malloc
はすべての最新のシステムでクリティカルセクションになる可能性は低いです。なぜなら、最新のシステムは システム内のスレッドごとに1つのメモリプールを保持するのに十分スマート N個のスレッドが単一のプールをめぐって戦います。 (sbrk
システムコールはおそらく重要なセクションですが、malloc
はその時間のほとんどをsbrk
に費やしません。またはmmap
、またはクールな子供たちが最近使用しています。)
re-entrancy は実際にはどういう意味ですか?基本的に、関数は安全に再帰的に呼び出すことができます—現在の呼び出しは2番目の呼び出しの実行中に「保留」され、最初の呼び出しは「中断したところから再開」できます。 (厳密には、これはmight再帰呼び出しによるものではありません。最初の呼び出しはスレッドAで行われる可能性があり、スレッドBによって途中で中断され、2番目の呼び出しが行われます。ただし、このシナリオは単なるthread-safetyの特殊なケースなので、この段落ではそれを忘れることができます。)
printf
もmalloc
もbeはできない可能性があります。これらはリーフ関数であるため(それらは自分自身を呼び出したり、ユーザーを呼び出したりしないため)再帰呼び出しを行う可能性のある制御されたコード)。また、上記で見たように、2001年以降は(ロックを使用することにより)*マルチスレッドの再入可能呼び出しに対してスレッドセーフでした。
したがって、printf
とmalloc
が再入不可であると言った人は誰でも間違っていました。彼らが言うつもりだったのは、おそらく両方とも、プログラムでクリティカルセクションになる可能性があるということです。一度に1つのスレッドしか通過できないボトルネックです。
Pedantic note:glibcは、printf
を使用して任意のユーザーコードを呼び出すことができる拡張機能を提供しています。これは、そのすべての順列で完全に安全です—少なくともスレッドセーフに関しては。 (明らかに、それは絶対にinsane format-string脆弱性への扉を開きます。)2つのバリアントがあります:_register_printf_function
_(ドキュメント化され、合理的に正気ですが、公式には「非推奨」)と_register_printf_specifier
_(これはほぼ文書化されていない1つの追加パラメータと ユーザー向けドキュメントの完全な欠如 を除いて同一です)。私はどちらもお勧めしませんが、ここでは興味深いこととして単に言及します。
_#include <stdio.h>
#include <printf.h> // glibc extension
int widget(FILE *fp, const struct printf_info *info, const void *const *args) {
static int count = 5;
int w = *((const int *) args[0]);
printf("boo!"); // direct recursive call
return fprintf(fp, --count ? "<%W>" : "<%d>", w); // indirect recursive call
}
int widget_arginfo(const struct printf_info *info, size_t n, int *argtypes) {
argtypes[0] = PA_INT;
return 1;
}
int main() {
register_printf_function('W', widget, widget_arginfo);
printf("|%W|\n", 42);
}
_
最も可能性が高いのは、printfへの別の呼び出しがまだそれ自体を印刷している間に出力の書き込みを開始できないためです。メモリの割り当てと割り当て解除についても同様です。