みなさんこんにちは。
今回はシグナル(signal)という機能を使って、UNIXにおけるプロセス間通信を実現していきます。
シグナル(signal)ってどんな機能なの?
UNIXのプロセス間通信を実現する主な手段には以下のようなものがあります。
・パイプ(pipe): 親プロセスと子プロセスを結ぶ
・シグナル(signal): 非同期的にソフトウェアに割り込む
・ソケット(socket): プロセス間でデータのやりとりをする
他にもセマフォ、メッセージなどがあります。
今回解説するシグナルという機能は、
実行中のプロセスに非同期的にイベントを通知したり、
シグナルを届けることで好きなタイミングで動作を行わせたりすることができます。
具体的には、
『Ctrl-Cを押してもプロセスを終了させない』
『killコマンドを送ってもプロセスを終了しないようにする』
などの動作をさせることができます。
システムプログラミングをしていく上では欠かせない重要な機能ですが、
非同期通信や競合状態などの難しい概念が絡んでくるので、正しい理解が必要です。
シグナルハンドラの設定をしてみよう!
シグナルの機能を使うためには、
どんなタイミングでシグナルを受け取って、
シグナルの種類に応じてどのような動作をするかということを記述しなければいけません。
それを実現する関数が、signal()やsigaction()などの関数です。
signal()の使い方
今回は主にsignal()について使い方を解説していきます。
1 2 3 4 |
#include <signal.h> void (*signal(int sig, void (*handler)(int)))(int); |
signal()は、
引数がint型、返り値がvoid型の関数へのポインタを返す関数で、
引数として、
int型と、int型を引数として受け取りvoid型を返り値として返す関数へのポインタを返します。
『全くわからん・・・・』と思ってしまった人は、
以下の記事でも説明していますが、まずはc言語における関数ポインタについて理解しましょう。
signal()の引数はsigとhandlerですが、
sigにはシグナル番号を、handlerにはsigで指定した番号のシグナルが来たときに実行する関数へのポインタを渡します。
このhandlerのことを、一般的にシグナルハンドラと呼んでいます。
signal()にこれらの引数を渡して実行すると、指定した番号のシグナルが来たときに自動的にシグナルハンドラが呼び出されます。
また返り値には、以前のシグナルハンドラの関数ポインタが返ってきます。
シグナルハンドラを指定する前はデフォルト動作が呼ばれる
signal()関数で、指定した番号のシグナルにシグナルハンドラを設定することができるわけですが
シグナルハンドラを何も設定していない(= signal()を実行していない)ときに、実行中のプロセスにシグナルが来た時、どのような挙動をするのでしょうか?
答えとしては『デフォルト動作』というものが呼び出されます。
デフォルト動作とは、
シグナル番号毎に決められた初期状態での動作のことで、以下の表のようになっています。
(Linux などで man signal と打つことで仕様を見ることができます。)
実際にsignal()を使ってシグナルハンドラを設定しよう
signal()の使い方を説明したので、さっそく使ってみることにしましょう。
以下のソースコードを実行してみてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <signal.h> /* signal */ #include <unistd.h> /* write */ void handler1(int signal) { } void handler2(int signal) { write(STDERR_FILENO, ".", 1); alarm(5); } int main() { signal(SIGINT, handler1); signal(SIGALRM, handler2); alarm(5); while(1) {} return 0; } |
このコードを実行すると、
『.(コンマ)』が5秒おきに出力され、Ctrl+Cを押してもプログラムを終了させることができない状態になっていると思います。
これは、
1回目のsignal()でCtrl+Cを押したときに飛ばされるSIGINTシグナルに対してhandler1()を、
2回目のsignal()でalerm()の残り時間が0になったときに飛ばされるSIGALRMシグナルに対してhandler2をしています。
ユーザーがCtrl+Cを押したときにデフォルト動作のままならプロセスが終了するのですが、
signal()によってhandler1()が呼び出されるようになっているので、プロセスを終了するのではなく、
handler1()の内容(=上記のコード例では何も行われない)が実行されるようになります。
alerm()の残り時間が0になったときも、
handler2()では『.(コンマ)』を出力し再びalerm()を実行しているので、ずっと『.(コンマ)』が出力されることになります。
シグナルを使うときは競合状態に気をつけよう
シグナルの使い方は関数ポインタを理解している人なら、
簡単に覚えられるものなのですが、注意しなければいけないことして『競合状態』というものがあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <stdio.h> #include <unistd.h> #include <signal.h> static volatile long long c1 = 0, c2 = 0, c3 = 0; void handler(int sig) { c1++; c2++; c3++; alarm(1); } int main(void) { signal(SIGALRM, handler); alarm(1); for (long long i = 0; i < 50000000000; i++) { c1++; c2++; c3++; } printf("%lld, %lld, %lld\n", c1, c2, c3); } |
このプログラムを実行してみると、以下のような結果になります。
c1、c2、c3は同時に1を足しているので普通に考えれば同じ結果が出力されるはずですよね。
しかし、実際には最後の3桁が『145』『129』『139』とどれも違う数字が出力されています。
こうなってしまうのは、実行中のプロセスの中で『競合状態』が起こっているからです。
競合状態が発生してしまうと、本来意図した結果とは異なる結果が返ってきてしまうので、回避しなければいけません。
競合状態を解決する方法
競合状態を解決するには、
競合状態の原因となるコード(=クリティカルセクション)を見つけ出して、競合状態にならないようにする必要があります。
上記のプログラムでは、main()関数内の
1 2 3 4 5 |
c1++; c2++; c3++; |
がクリティカルセクションとなります。
今回はこれの処理中にシグナルに割り込まれないようにする(=相互排他:共通資源へのアクセスを同期的に行う)ことで、解決していきます。
シグナルをブロックするにはsigprocmask()という関数を使いますが、以下のプログラムのようにして使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#include <stdio.h> #include <unistd.h> #include <signal.h> static volatile long long c1 = 0, c2 = 0, c3 = 0; void handler(int sig) { c1++; c2++; c3++; alarm(1); } int main(void) { sigset_t set, oset; sigemptyset(&set); // シグナルマスクの初期化 sigaddset(&set, SIGALRM); // シグナルマスクへシグナルを追加する signal(SIGALRM, handler); alarm(1); for (long long i = 0; i < 50000000; i++) { sigprocmask(SIG_BLOCK, &set, &oset); // ブロック c1++; c2++; c3++; sigprocmask(SIG_SETMASK, &oset, NULL); // ブロック解除 } printf("%lld, %lld, %lld\n", c1, c2, c3); } |
このプログラムを実行すると、以下のような結果になります(1ループあたりに処理速度がかなり遅くなるので、ループ回数を減らしています)
sigemptyset()とsigaddset()でシグナルマスクを生成したあと、
クリティカルセクションの最初にsigprocmask(SIG_BLOCK, ….)を書いて、最後にsigprocmask(SIG_SETMASK, …)を書くことで競合状態が起きないようにしています。
まとめ
今回はシグナル(signal)というUNIXのプロセス間通信の方法の一つについて解説してきましたが、いかがだったでしょうか?
シグナルは使いこなせば普通のプログラミングではできないことができる一方で、
何も考えずに使用していると競合状態が発生することがあり、難しいところもあります。
競合状態を避けるためにはクリティカルセクションを見つけて相互排他する必要があるので、注意が必要です。
最後まで読んでいただきありがとうございました。