4

私はいくつかのコースワークを行っています、そして私たちは次のコードを提示されました。いくつかの質問は、コードのさまざまな行が何をするのかを尋ねますが、それは問題ありません。私は理解していますが、カーブボールは「このプログラムには競合状態が含まれています。どこで、なぜ発生するのですか?」

コード:

#include <stdio.h>
#include <signal.h>

static void handler(int signo) { 
    printf("This is the SIGUSR1 signal handler!\n");
}

int main(void) { 
    sigset_t set;
    sigemptyset(&set);
    sigset(SIGUSR1, handler);
    sigprocmask(SIG_SETMASK, &set, NULL);

    while(1) {
        printf("This is main()!\n");
    }
    return 0;

}

信号が到着したときに「Thisismain」または「ThisisSIGUSR1」がどの順序で出力されるかを知る方法がないという競合状態だと思いますが、誰かがこれを確認または明確にできれば私はよろしくお願いします。彼はまた、完全な答えを探すのではなく、それをどのように修正できるか(競合状態)を尋ねますが、ヒントをいただければ幸いです。

4

2 に答える 2

7

競合状態は実際にはありません。それより悪いです。POSIX標準によれば、プログラムの動作は定義されていません(信号が適切なタイミングで配信される場合)。

man 7のsignalmanページ、特にAsync-signalsafefunctionsの下のセクションを見てください。

プログラム実行中の任意の時点で他の場所での処理が中断される可能性があるため、シグナルハンドラ関数は非常に注意する必要があります。POSIXには「安全機能」の概念があります。シグナルが安全でない関数の実行を中断し、ハンドラーが安全でない関数を呼び出す場合、プログラムの動作は定義されていません。

これは、非同期信号の安全な関数printf()ではないことに注意してください。したがって、動作は定義されていません。

一般的なケースでは、非同期信号の安全なロックプリミティブがないため、解決策は簡単ではありません(sem_post()これだけでは不十分であり、すべてのprintf()呼び出しで使用する必要があるファイルロック以外)。一般的なポータブルソリューションは、pipe()fromを使用してパイプを作成し、を使用しunistd.hて出力をパイプに書き込み、write()メインプログラムにパイプから読み取り、コンテンツを「転送」させることです。POSIXPIPE_BUFは、アトミックよりも短く書き込みをPIPE_BUF行い、少なくとも512(Linuxでは4096)man 7 pipeであるため、詳細を参照してください。したがって、ポータブルコードの場合、実際には512バイト以下のメッセージに制限されます。

printf()一般的に、そしてこの特定のケースでは、シグナルハンドラーのをグローバルvolatile sigatomic_t変数の設定に置き換えるだけで十分です。メインループは、グローバル変数をチェック(およびクリア)して、メッセージ自体を出力するだけです。

フラグ変数アプローチは急速に繰り返される信号を失う可能性がありますが、常に 急速に繰り返される信号を失うSIGUSR1可能性があるため、関係ありません。一度に保留できるのは1つだけであるため、最初の信号と処理時に発生する繰り返し信号は、すべて!(キューに入れられているようなリアルタイムシグナルを使用する場合は、メインループやシグナルハンドラーなどのアトミックビルトインを使用して、すべてのシグナルを確実にキャッチできます。変更がすぐに有効になり、他のユーザーに表示されることを確認してください。ただし、この場合、アトミック操作について心配する必要はありません。)SIGUSR1SIGRTMIN+0__sync_fetch_and_and(variable,0)__atomic_exchange_n(variable,0,__ATOMIC_SEQ_CST)__sync_fetch_and_add(variable,1)__atomic_fetch_add(variable,1,__ATOMIC_SEQ_CST)__sync_synchronize()__atomic_signal_fence(__ATOMIC_SEQ_CST)

sigset()およびに関しても、競合状態ではなく、興味深いコーナーケースがありsigprocmask()ます。プロセスはその親からシグナルマスクを継承し、SIGUSR1デフォルトではブロックされません。処理されない限り、プロセスは終了します。したがって、継承されたシグナルマスクに応じてSIGUSR1、コールの前に配信されたシグナルsigset()がブロックされるか、プロセスが終了します。(ただし、set含まれている場合SIGUSR1、つまりSIGUSR1ブロックされている場合sigprocmask()、前に呼び出されない限り、競合状態が発生しsigset()ます。ただし、setが空でsigset()あるため、前に呼び出すのが最適sigprocmask()です。)

于 2012-08-05T12:36:34.683 に答える
5

どうやらこのコースの目的は、コードを次のように変更することです。

  • sigwait()別のスレッドを使用します。このスレッドは、呼び出し元またはsigwaitinfo()ループ内の信号を受信します。シグナルをブロックする必要があります(すべてのスレッドで最初に、そしてずっと)、または操作が指定されていません(またはシグナルが他のスレッドに配信されます)。

    このように、シグナルハンドラー関数自体はなく、非同期シグナルセーフ関数に制限されます。sigwait()/を呼び出すスレッドsigwaitinfo()は完全に通常のスレッドであり、シグナルまたはシグナルハンドラーに関連する制限はありません。

    (たとえば、グローバルフラグを設定し、ループがチェックするシグナルハンドラーを使用して、シグナルを受信する方法は他にもあります。これらのほとんどは、ビジーウェイト、何もしないループの実行、CPU時間を無駄に消費することにつながります。非常に悪い解決策ここで説明する方法では、CPU時間を浪費しません。カーネルはsigwait()/sigwaitinfo()を呼び出すとスレッドをスリープ状態にし、信号が到着したときにのみスレッドをウェイクアップします。スリープの期間を制限したい場合は、次のことができます。sigtimedwait()代わりに使用してください。)

  • 以来printf()。スレッドセーフであることが保証されているわけではありません。おそらく、を使用しpthread_mutex_tて出力を標準出力に保護する必要があります。つまり、2つのスレッドがまったく同時に出力しようとしないようにする必要があります。

    Linuxでは、GNU C printf()(バージョンを除く_unlocked())はスレッドセーフであるため、これは必要ありません。これらの関数を呼び出すたびに、すでに内部ミューテックスが使用されています。

    Cライブラリは出力をキャッシュする可能性があるため、データが確実に出力されるようにするには、を呼び出す必要があることに注意してくださいfflush(stdout);

    printf()ミューテックスは、他のスレッドが間に出力を挿入できないように、複数のfputs()、、または同様の呼び出しをアトミックに使用する場合に特に便利です。したがって、Linuxでは単純なケースでは必要ない場合でも、ミューテックスをお勧めします。(もちろん、ミューテックスを保持している間もやりたいと思いますがfflush()、出力がブロックされるとミューテックスが長時間保持される可能性があります。)

私は個人的に問題全体をまったく異なる方法で解決しますwrite(STDERR_FILENO,)。シグナルハンドラーで標準エラーに出力し、メインプログラムを標準出力に出力します。スレッドや特別なものは必要ありません。シグナルハンドラーの単純な低レベルの書き込みループだけです。厳密に言えば、私のプログラムの動作は異なりますが、エンドユーザーには結果はほとんど同じに見えます。(出力を別のターミナルウィンドウにリダイレクトして並べて表示したり、各入力行にナノ秒の壁掛け時計のタイムスタンプを追加するヘル​​パースクリプト/プログラムにリダイレクトしたりできることを除いて、調査時に役立つその他の同様のトリックもの。)

個人的には、元の問題から「正しい解決策」へのジャンプを見つけました。実際に私が説明しているのが正しい解決策である場合。私はそれが-少しストレッチになると信じています。このアプローチは、Safが正しいソリューションでpthreadを使用することが期待されていると述べたときに初めて私に気づきました。

これが参考になることを願っていますが、ネタバレではありません。


2013年3月13日編集:

これはwritefd()、シグナルハンドラーから記述子にデータを安全に書き込むために使用する関数です。また、ラッパー関数も含めましたwrout()wrerr()これを使用して、文字列を標準出力または標準エラーにそれぞれ書き込むことができます。

#include <unistd.h>
#include <string.h>
#include <errno.h>

/**
 * writefd() - A variant of write(2)
 *
 * This function returns 0 if the write was successful, and the nonzero
 * errno code otherwise, with errno itself kept unchanged.
 * This function is safe to use in a signal handler;
 * it is async-signal-safe, and keeps errno unchanged.
 *
 * Interrupts due to signal delivery are ignored.
 * This function does work with non-blocking sockets,
 * but it does a very inefficient busy-wait loop to do so.
*/
int writefd(const int descriptor, const void *const data, const size_t size)
{
    const char       *head = (const char *)data;
    const char *const tail = (const char *)data + size;
    ssize_t           bytes;
    int               saved_errno, retval;

    /* File descriptor -1 is always invalid. */
    if (descriptor == -1)
        return EINVAL;

    /* If there is nothing to write, return immediately. */
    if (size == 0)
        return 0;

    /* Save errno, so that it can be restored later on.
     * errno is a thread-local variable, meaning its value is
     * local to each thread, and is accessible only from the same thread.
     * If this function is called in an interrupt handler, this stores
     * the value of errno for the thread that was interrupted by the
     * signal delivery. If we restore the value before returning from
     * this function, all changes this function may do to errno
     * will be undetectable outside this function, due to thread-locality.
    */
    saved_errno = errno;

    while (head < tail) {

        bytes = write(descriptor, head, (size_t)(tail - head));

        if (bytes > (ssize_t)0) {
            head += bytes;

        } else
        if (bytes != (ssize_t)-1) {
            errno = saved_errno;
            return EIO;

        } else
        if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) {
            /* EINTR, EAGAIN and EWOULDBLOCK cause the write to be
             * immediately retried. Everything else is an error. */
            retval = errno;
            errno = saved_errno;
            return retval;
        }
    }

    errno = saved_errno;
    return 0;
}

/**
 * wrout() - An async-signal-safe alternative to fputs(string, stdout)
 *
 * This function will write the specified string to standard output,
 * and return 0 if successful, or a nonzero errno error code otherwise.
 * errno itself is kept unchanged.
 *
 * You should not mix output to stdout and this function,
 * unless stdout is set to unbuffered.
 *
 * Unless standard output is a pipe and the string is at most PIPE_BUF
 * bytes long (PIPE_BUF >= 512), the write is not atomic.
 * This means that if you use this function in a signal handler,
 * or in multiple threads, the writes may be interspersed with each other.
*/
int wrout(const char *const string)
{
    if (string)
        return writefd(STDOUT_FILENO, string, strlen(string));
    else
        return 0;
}

/**
 * wrerr() - An async-signal-safe alternative to fputs(string, stderr)
 *
 * This function will write the specified string to standard error,
 * and return 0 if successful, or a nonzero errno error code otherwise.
 * errno itself is kept unchanged.
 *
 * You should not mix output to stderr and this function,
 * unless stderr is set to unbuffered.
 *
 * Unless standard error is a pipe and the string is at most PIPE_BUF
 * bytes long (PIPE_BUF >= 512), the write is not atomic.
 * This means that if you use this function in a signal handler,
 * or in multiple threads, the writes may be interspersed with each other.
*/
int wrerr(const char *const string)
{
    if (string)
        return writefd(STDERR_FILENO, string, strlen(string));
    else
        return 0;
}

ファイル記述子がパイプを参照している場合、アトミックに最大(少なくとも512)バイト writefd()を書き込むために使用できます。I / Oを多用するアプリケーションで使用して、信号(および、関連する値、整数、またはポインターを使用して発生した場合)をソケットまたはパイプ出力(データ)に変換し、複数のI/Oをより簡単に多重化することもできます。ストリームと信号処理。バリアント(close-on-execとマークされた追加のファイル記述子を持つ)は、子プロセスが別のプロセスを実行したか、失敗したかを簡単に検出するためによく使用されます。そうしないと、どのプロセス(元の子、または実行されたプロセス)が終了したかを検出するのが困難になります。PIPE_BUFwritefd()sigqueue()

この回答へのコメントでは、についての議論があり、変更するerrnoという事実がシグナルハンドラーに適さないかどうかについて混乱がありました。write(2)errno

まず、POSIX.1-2008(およびそれ以前)では、非同期シグナルセーフ関数をシグナルハンドラーから安全に呼び出すことができる関数として定義しています。2.4.3シグナルアクションの章には、を含むそのような機能のリストが含まれていますwrite()また、 「errnoの値を取得する操作、およびerrnoに値を割り当てる操作は、非同期信号に対して安全である」と明示的に記載されていることに注意してください。

つまり、POSIX.1はwrite()シグナルハンドラーで安全に使用できるように意図されておりerrno、中断されたスレッドで予期しない変更が発生するのを防ぐために操作することもできますerrno

はスレッドローカル変数であるためerrno、各スレッドには独自のがありerrnoます。シグナルが配信されると、プロセス内の既存のスレッドの1つが常に中断されます。シグナルは特定のスレッドに送信できますが、通常、カーネルはどのスレッドがプロセス全体のシグナルを取得するかを決定します。システムによって異なります。初期スレッドまたはメインスレッドのスレッドが1つしかない場合は、明らかにそれが中断されます。これはすべて、シグナルハンドラーが最初に認識した値を保存し、errno戻る直前に復元した場合、への変更errnoはシグナルハンドラーの外部では表示されないことを意味します。

ただし、これを検出する方法は1つありますが、POSIX.1-2008でも、注意深い表現によって示唆されています。

技術的に&errnoは、ほとんどの場合有効であり(システム、コンパイラ、および適用される標準によって異なります)、int現在のスレッドのエラーコードを保持している変数のアドレスを生成します。したがって、別のスレッドが別のスレッドのエラーコードを監視する可能性があります。そうです、このスレッドは、シグナルハンドラー内で行われた変更を確認します。ただし、他のスレッドがエラーコードにアトミックにアクセスできるという保証はありません(ただし、多くのアーキテクチャではアトミックです)。このような「監視」は、とにかく情報を提供するだけです。

Cのほとんどすべてのシグナルハンドラーの例がstdio.hetceteraを使用しているのは残念ですprintf()。非非同期セーフからキャッシングの問題、FILE中断されたコードが同時にI / Oを実行している場合は、フィールドへの非アトミックアクセスまで、多くのレベルで間違っているだけでなく、正しい解決策unistd.hこの編集での私の例と同様の使用は、同じように簡単です。シグナルハンドラーでstdio.hI/ Oを使用する理由は、「通常は機能する」ようです。個人的には、たとえば暴力も「通常は機能する」ので、私はそれを嫌います。私はそれを愚かで怠惰だと思います。

これがお役に立てば幸いです。

于 2012-08-06T20:24:55.493 に答える