なぜmalloc()、printf()として再入不可能な?
-
30-09-2019 - |
質問
UNIXシステムとして知ら malloc()
は再入不可能な機能(システムます。この理由は何でしょうか?
同様に、 printf()
もと行うことができるものとします。入;なぜですか?
ピックアップしました。定義の再entrancyが知りたかったんですがなぜこの制度の対象となるこれらの機能を何を防止し再入可能な保証?
解決
malloc
や printf
は、通常、グローバル構造を採用するロックに基づく同期内蔵しています。そこではありませんが再入可能な.
の malloc
機能とはスレッドセーまたはスレッド安全ではない。いるのではなく再入可能な:
Malloc社のグローバルヒープまれてくる可能性があることで異なる二つのメソッドの呼び出しの
malloc
うと同時に戻り、同一のメモリブロックです。(2malloc話すべきである前にアドレスのチャンクを取得すが、チャンクされたとして不可)。この違反にpostconditionの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
だから異なるスレッド)。そのreentrancy概念を超えたスレッドの安全性及び必要機能が正常に動作し 場合でも、その呼び出しで終了しない.その基本的なぜ他の機能とロックをするとはできませんの応募者.
の printf
機能が運営するグローバルデータです。あらゆる出力ストリームを採用し、通常のグローバルバッファのリソースデータ送(バッファーターミナル、またはファイル).印刷工程では、通常のシーケンスをコピーデータバッファをフラッシングのバッファ。このバッファに保護すべきロックと同様に malloc
います。そのため、 printf
でも再入不可能な.
他のヒント
私たちが何を意味するのか理解しましょう 再入国. 。再入力機能は、以前の呼び出しが終了する前に呼び出すことができます。これは場合に発生する可能性があります
- 関数は、関数の実行中に発生した信号の信号ハンドラー(またはunix unix inutrudurterハンドラー)で呼び出されます
- 関数は再帰的に呼ばれます
Mallocは、フリーメモリブロックを追跡するいくつかのグローバルデータ構造を管理しているため、再入力していません。
Printfは、グローバル変数、つまりファイル* Stoutのコンテンツを変更するため、再入力しません。
ここには少なくとも3つの概念があり、そのすべてが口語的な言葉で混同されているため、混乱していたのかもしれません。
- スレッドセーフ
- 重要なセクション
- 再入国
最も簡単なものを最初に取るには: 両方 malloc
と printf
それは スレッドセーフ. 。それらは、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);
}
ある関数の例 スレッドセーフではありません は strtok
. 。あなたが電話するなら strtok
同時に2つの異なるスレッドから、結果は未定義の動作です。 strtok
静的バッファーを内部的に使用して、状態を追跡します。 glibcが追加します strtok_r
この問題を修正するために、C11は同じことを追加しました(ただし、ここで発明されていないため、オプションで別の名前で) strtok_s
.
さて、でもそうではありません printf
グローバルリソースを使用して出力を構築しますか?実際、それは何ですか 平均 2つのスレッドからstdoutに印刷します 同時に? それは私たちを次のトピックにもたらします。明らかに printf
aになります 重要なセクション それを使用するプログラムで。 実行のスレッドは、一度に重要なセクション内にあることが許可されています。
少なくともPOSIXに準拠したシステムでは、これは printf
通話から始めます flockfile(stdout)
そして、電話で終わります funlockfile(stdout)
, 、基本的には、stdoutに関連するグローバルミューテックスを取得するようなものです。
ただし、それぞれ異なる FILE
プログラムでは、独自のミューテックスを持つことが許可されています。これは、1つのスレッドが呼び出すことができることを意味します fprintf(f1,...)
同時に、2番目のスレッドが呼び出しの途中にあると同時に fprintf(f2,...)
. 。ここには人種の状態はありません。 (あなたのLIBCが実際にこれらの2つの呼び出しを並行して実行しているかどうかは Qoi 問題。私は実際にGlibcが何をしているのかわかりません。)
同様に、 malloc
現代のシステムは システム内の各スレッドに1つのメモリプールを保持するのに十分なスマート, 、すべてのnスレッドが単一のプールで戦うのではなく。 ( sbrk
システムコールはおそらく重要なセクションですが、 malloc
その時間はほとんどありません sbrk
. 。または mmap
, 、またはクールな子供たちが最近使用しているものは何でも。)
そして 何をしますか 再び危機に包まれます 実際には? 基本的に、それは関数を安全に再帰的に呼ぶことができることを意味します - 現在の呼び出しは「保留にされている」のは2回目の呼び出しであり、最初の呼び出しはまだ「中断したところから拾う」ことができます。 (技術的にはこれ そうかもしれない 再帰的な呼び出しによるものではありません。最初の呼び出しはスレッドAにある可能性があります。スレッドAは、2回目の呼び出しを行うスレッドBで中央で中断されます。しかし、そのシナリオは単なる特別なケースです スレッドセーフティ, 、そのため、この段落でそれを忘れることができます。)
ない printf
または malloc
可能性があります なれ 葉の機能であるため、単一のスレッドで再帰的に呼ばれます(再帰的な呼び出しを行う可能性のあるユーザー制御コードに電話をかけたり、呼び出したりすることはありません)。そして、上で見たように、彼らは2001年以降(ロックを使用して) *マルチ *スレッドリエントラントコールに対してスレッドセーフをしています。
だから、誰があなたにそれを言った printf
と malloc
非レントラントが間違っていたのでした。彼らが言ったのは、おそらく彼らの両方が 重要なセクション あなたのプログラムでは、一度に1つのスレッドのみを通過できるボトルネック。
Pedantic Note:glibcは拡張機能を提供します printf
自らを再コールするなど、任意のユーザーコードを呼び出すことができます。これは、すべての順列において完全に安全です - 少なくとも糸の安全性に関する限り。 (明らかに、それは絶対に扉を開きます 非常識 フォーマットストリングの脆弱性。)2つのバリエーションがあります。 register_printf_function
(これは文書化されており、合理的に正気ですが、公式には「非推奨」) register_printf_specifier
(これは ほとんど 文書化されていない1つのパラメーターとaを除いて同一 ユーザー向けドキュメントの完全な欠如)。どちらもお勧めしません。ここでは、単に興味深いものとして言及しています。
#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への別の呼び出しがまだそれを印刷している間に出力の書き込みを開始できないからです。同じことがメモリの割り当てと取引にも当てはまります。
これは、両方ともグローバルリソース(ヒープメモリ構造とコンソール)で機能するためです。
編集:ヒープは、親切なリンクリスト構造に他なりません。各 malloc
また free
それを変更するため、アクセスを書き込むことでいくつかのスレッドを同時に持つことで、その一貫性が損傷します。
edit2:別の詳細:Mutexesを使用してデフォルトでリエントラントにすることができます。しかし、このアプローチは費用がかかり、MT環境で常に使用されるというガランティはありません。
したがって、2つの解決策があります。2つのライブラリ関数、1つはリエントラント、もう1つはNotまたはMutexパーツをユーザーに残しておくか、1つはまったく残します。彼らは2番目を選びました。
また、これらの関数の元のバージョンが非レントラントであったため、互換性のために宣言されたためです。
2つの別々のスレッドからmallocを呼び出すと(C標準で保証されていないスレッドセーフバージョンがない限り)、2つのスレッドに1つのヒープしかないため、悪いことが起こります。 printfについても同じ - 動作は未定義です。それが彼らを現実に非レントラントにしている理由です。