web-dev-qa-db-ja.com

malloc()とprintf()が再入不可能と言われるのはなぜですか?

UNIXシステムでは、malloc()が非再入可能関数(システムコール)であることがわかっています。何故ですか?

同様に、printf()も非再入可能と呼ばれます。どうして?

私は再入可能性の定義を知っていますが、それがこれらの関数に適用される理由を知りたいと思いました。それらが再入可能であることが保証されないのは何ですか?

37
ultimate cause

mallocおよびprintfは通常、グローバル構造を使用し、内部でロックベースの同期を採用します。そのため、それらは再入可能ではありません。

malloc関数は、スレッドセーフまたはスレッドセーフでない可能性があります。どちらも再入可能ではありません。

  1. Mallocはグローバルヒープで動作し、同時に発生するmallocの2つの異なる呼び出しが同じメモリブロックを返す可能性があります。 (2番目のmalloc呼び出しは、チャンクのアドレスがフェッチされる前に行われる必要がありますが、チャンクは使用不可としてマークされていません)。これはmallocの事後条件に違反しているため、この実装は再入できません。

  2. この影響を防ぐために、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も再入不可です。

56
P Shved

リエントラント の意味を理解しましょう。再入可能関数は、前の呼び出しが完了する前に呼び出すことができます。これは、

  • 関数は、関数の実行中に発生したシグナルのシグナルハンドラー(またはより一般的にはUnixのいくつかの割り込みハンドラー)で呼び出されます
  • 関数が再帰的に呼び出された

mallocは空きメモリブロックを追跡するいくつかのグローバルデータ構造を管理しているため、再入可能ではありません。

printfはグローバル変数、つまりFILE * stoutの内容を変更するため、再入可能ではありません。

12
JeremyP

ここには少なくとも3つの概念があり、それらすべてが口語的な言語で混同されているため、混乱していたのかもしれません。

  • スレッドセーフ
  • クリティカルセクション
  • 再入可能

最も簡単な方法を最初に使用するには:_mallocprintfの両方が 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準拠のシステムでは、これはprintfflockfile(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の特殊なケースなので、この段落ではそれを忘れることができます。)

printfmallocbeはできない可能性があります。これらはリーフ関数であるため(それらは自分自身を呼び出したり、ユーザーを呼び出したりしないため)再帰呼び出しを行う可能性のある制御されたコード)。また、上記で見たように、2001年以降は(ロックを使用することにより)*マルチスレッドの再入可能呼び出しに対してスレッドセーフでした。

したがって、printfmallocが再入不可であると言った人は誰でも間違っていました。彼らが言うつもりだったのは、おそらく両方とも、プログラムでクリティカルセクションになる可能性があるということです。一度に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);
}
_
4
Quuxplusone

最も可能性が高いのは、printfへの別の呼び出しがまだそれ自体を印刷している間に出力の書き込みを開始できないためです。メモリの割り当てと割り当て解除についても同様です。

1
stdan28