42のminitalkについて
この記事では42というプログラミングスクール?的なところでの課題であるminitalkについてのまとめ、コードの意味などをまとめておく。(bonusはやってないです)
この課題はserverを立ち上げて、そこに対してclient側から文字を1bitずつ送信してserver側で出力する、という課題である
server
まずはserver側から説明していく。
int main(void)
{
int pid;
struct sigaction sa;
pid = getpid();
ft_printf("server pid = %d\n", pid);
sa.sa_handler = handle_signal;
sa.sa_flags = 0;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGUSR1, &sa, NULL) == -1)
{
perror("sigaction");
exit(EXIT_FAILURE);
}
if (sigaction(SIGUSR2, &sa, NULL) == -1)
{
perror("sigaction");
exit(EXIT_FAILURE);
}
while (1)
{
pause();
}
return (0);
}
void handle_signal(int sig)
{
static int bit_count = 0;
static char current_char = 0;
if (sig == SIGUSR1)
current_char |= (1 << bit_count);
bit_count++;
if (bit_count == 8)
{
write(1, ¤t_char, 1);
bit_count = 0;
current_char = 0;
}
}
includeなどは省略したが大体こんな感じ。
ft_printfとなっているのはprintfを以前自分が実装したものを使用している。普通のprintfと挙動は変わらないが、課題が正規のprintfを使っちゃダメって言ってるので以前作成したものを呼び出している。
ではコードを見ていく。
struct sigaction sa;
まずここでは、sigactionというシグナルハンドラを設定するための構造体を宣言する。シグナルとは、あるプロセス(実行中のプログラム)に対して、特定のイベントが発生したことを通知するためのソフトウェア割り込み。
ソフトウェア割り込みについて
その前にプロセス割り込みについても話しておく。
プロセス割り込みとは、実行中のプロセスの通常のフローを中断して、特定のイベントに対して即座に処理を行うためのシステム。プロセス割り込みの中にはハードウェア割り込みとソフトウェア割り込みの2種類がある。
ハードウェア割り込み
ハードウェアデバイス、例えばキーボードやマウスなど、から行われる割り込み。
例えば、私たちがCPUを使用している際はOSやアプリケーションなどの複数のプロセスが実行中ですが、その際にマウスを動かすとそれに追従して画面内のポインタが動かされる。これは実はCPUのプロセスと同時に実行しているのではなく、CPUの状態を一時保存して、マウスの移動量などを計算し、それに対するポインタを動かした上で一時保存したCPUのプロセスを再開しているのである。この一連の動作は一瞬で行われるため、同時に実行されているように思えるかもしれないが、実はハードウェア割り込みが起こっている。
ソフトウェア割り込み
これはソフトウェアによって発生する割り込み。シグナルやシステムコールがこれに該当する。
例えば、何か実行中のプロセス、ターミナル上でプログラムを実行中の際にCtrl+C
などを押すとそのプログラムが中止されると思うが、これはSIGINT(SIGnal INTerrput)と呼ばれるシグナルが送信されているためである。デフォルトではプロセスの終了時に実行されるのだが、ソフトウェア割り込みによって現在フォアグラウンドで実行中のプロセスに対して、SIGINTが渡されプロセスが中断されるのである。
元の話題に戻るが、ソフトウェア割り込みなどで渡されるシグナルに対して、どの関数が実行されるか(シグナルハンドラ)などを定義するための構造体がsigactionという構造体で、それを先に定義しておく。
struct sigaction {
void (*sa_handler)(int); // シグナルを受信した時に実行される関数を指定する
void (*sa_sigaction)(int, siginfo_t *, void *); // sa_flagsにSA_SIGINFOが入っているときに使用され、シグナル番号、シグナルに関する追加情報を含む
sigset_t sa_mask; // シグナルハンドラが実行中に他のシグナルが割り込まないようにするために使用されている
int sa_flags; // シグナルハンドラの動作を制御するフラグ
void (*sa_restorer)(void); // 今はあんま使われてないらしい
};
sigaction構造体はこのようになっている。大まかにはこんな感じだ。細かい説明は使う時に述べる。
次に進む。
課題ではPID(process ID)を表示させて、そこのpidを指定してclient側から文字を送る。
なのでgetpidという関数を用いて現在実行しているプロセスを取得して出力する。
次にsigaction構造体のそれぞれの要素について定義していく
まずはsa_handler
を定義する。
これは、シグナルを受け取った時に実行される関数を定義している。今回はhandle_signal
という関数を作成しており、それを指定している。
次に、sa_flags
を設定する。
ここでは主によく使用されるらしいフラグの紹介に留めておく。
SA_RESTART
シグナルハンドラが終了した後、割り込まれたシステムコールを再開する。
例としてはシグナルが発生しても、read や write などのシステムコールが中断されずに再開されるような時など。
SA_SIGINFO
sa_handler の代わりに sa_sigaction メンバーを使用して拡張シグナルハンドラを設定する。
拡張シグナルハンドラは、シグナル番号、シグナルに関する追加情報を含む siginfo_t 構造体、およびコンテキスト情報を含むポインタを引数として受け取る。
SA_NOCLDSTOP
子プロセスが停止したときに SIGCHLD シグナルを送信しない。
など色々あるらしいが今回はそれらを使用しないので明示的に0として、デフォルトの動作であることを明記した。今回のsa_handlerは単純にビットを集めて文字を構成し、8ビットが揃ったらその文字を出力するだけのシンプルなものであり、この場合、特別なフラグを設定する必要はないと思う。
次は
sigemptyset(&sa_mask);
について説明する
まずはsa_maskについて
sa_mask は、シグナルハンドラが実行されている間にブロックされるシグナルのセットを指定する。シグナルハンドラが実行中に他のシグナルが割り込まないようにするために使用する。シグナルとは受け取ったプロセスがある特定の動作をするが、その動作中にさらに別のシグナルが送られてきた時に、実行中であったシグナルの動作をブロックするかどうかを指定するというもの。
sigemptyset(&sa.sa_mask) を使用することで、sa_mask を空のシグナルセットに初期化する。つまりこれにより、シグナルハンドラが実行されている間にブロックされるシグナルがないことを示す。
今回のsa_hanlderは8ビット揃ったら出力するだけのシンプルなものなので何も指定しない。また、指定しないことで、シグナルを連続で処理することができるし、ブロックすることで起こる可能性があるデッドロックなどを回避することができる。
また、man sigemptyset
などとするとERRORSのところにnot detectedと書かれており、基本的に失敗することがないと書かれているので、今回はエラーハンドリング等はしていない。一応正しくできた時は0が返ってきて、それ以外の時は-1が返ってくるらしいが失敗しないんらいらないかなって。
次はsigactionところだ。
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum: 設定するシグナル番号(例:SIGUSR1, SIGUSR2など)。
act: 設定するシグナルの動作を含む sigaction 構造体へのポインタ。
oldact: 以前の設定を取得したい場合に使用する sigaction 構造体へのポインタ(ここでは使わないので NULLに設定)。
戻り値: 正常終了時には0を返し、エラーが発生した場合には-1を返す。
そして、今回はsignumにはSIGUSER1とSIGUSER2を使用し、actポインタには先ほど設定したsa構造体を入れる。SIGUSER1やSIGUSER2は使用者、今回だと私たち、が自由に動作を決めて良いシグナルで、 handle_signal関数内で、SIGUSER1が来た時にはビットを1として、SIGUSER2が来た時にはビットを0とする。こうすることで、8ビットが来るまで0と1を自由に入れることができ、8ビット貯まったら文字を出力する。
失敗した時はperrorという関数でその引数内の文字列を標準出力する。
そしてexit関数でプログラムを終了するのだが、その際にEXIT_FAILUREをexit関数に渡すことで、プログラムがエラーで終了したことを示す。
そしてpause関数を無限ループさせることで、シグナルが来るまで待機させることができる。
pause関数とはシグナルを受信するまでプロセスを一時停止して、シグナルを受信したらシグナルハンドラを実行する。その後またpause関数が呼び出されて、次のシグナルを待機する。
handle_signal関数について
この関数では8ビットつまり1バイトが揃ったら文字を出力する。ASCIIでは8ビットで0と1の2通りの表し方があるので、128通りの文字を表せる。なので、8ビット揃ったら1文字を出力することができる。
bit_countは現在のビットの位置を追跡するためのカウンターであり、current_charはそのビットで表される文字を示す。静的変数にすることで呼び出すたびに初期化されないのでbitの位置などを正しく追跡することが可能となる。
特に難しいところはなく、ビット演算がちょっとわかりづらいくらいだろう。
|=
はOR演算というもので、
current_char = 1 << bit_count;
などとすると、current_char
のすべてのビットをクリアし、特定のビットだけを1に設定してしまう。なのでOR演算を用いることで、既存の情報を保持しながら新しいbit_countのところに1を入れることができる。
1 << bit_count
は右からbit_count目までシフトしてそこに1を代入する、ということである。
デフォは0だから何にも指定しなければ勝手に0が入るので今回は入れてないけど、入れてもいいかも。
これでserverの動作は終わり。
client
ここでは指定したプロセスに対して文字を1ビットずつ送信する。
int main(int argc, char **argv)
{
pid_t pid;
char *message;
if (argc != 3)
{
ft_printf("Usage: %s <pid> <string>\n", argv[0]);
return (1);
}
message = argv[2];
pid = atoi(argv[1]);
while (*message)
{
send_char(pid, *message);
message++;
}
send_char(pid, '\0');
return (0);
}
void send_char(int pid, char c)
{
int i;
i = 0;
while (i < 8)
{
if (c & (1 << i))
{
if (kill(pid, SIGUSR1) == -1)
{
perror("kill");
exit(EXIT_FAILURE);
}
}
else
{
if (kill(pid, SIGUSR2) == -1)
{
perror("kill");
exit(EXIT_FAILURE);
}
}
i++;
usleep(100);
}
}
まずは文字列を受け取って、それが正しい使い方(コマンドライン引数が3つかどうか)じゃなければ使い方を教える。
もし正しい使い方ならmessage(送信する文字列)を送り終えるまでsend_char関数を使い続ける。
そして最終的にnull文字を送って終わる。
send_char
mainは見ての通りなので本質的なsend_charを見ていく。
この関数では1ビットずつ指定されたプロセスに送信していく。それを8ビット分貯まるまで繰り返し、それで1文字(1バイト)送ったことになる。それを全文字分繰り返す。
その文字がiビット目が1ならSIGUSER1を送信し、0ならSIGUSER2を送信する。server側でもSIGUSER1なら1を受け取ったと解釈するようにしているので、それさえ合わせておけばSIGUSER2で1を意味して送信しても何も問題ない。
if (c & (1 << i))
{
if (kill(pid, SIGUSR1) == -1)
{
perror("kill");
exit(EXIT_FAILURE);
}
}
ここについて詳しく説明する。
まず
if (c & (1 << i))
だが、AND演算子(&)を用いて受け取った文字cのiビット目が1かどうかを確認して、もし1ならSIGUSER1を送る動作に入る。
このAND演算子とはcを8ビットで表した時に右からiビット目が1かどうかを確認している。1は8ビットで表すと00000001なのでそれをiビット左にシフトしてcの右からiビットめも1ならtureを、もしcのiビット目が0ならfalseを返す。
そしてkillシステムコールを用いて指定したプロセスに送信し、失敗したら-1が返ってくるのでerrorハンドリングする。
int kill(pid_t pid, int sig);
killという名前から誤解されがちだが、何かのプロセスを終了するのではなく、シグナルを送信するために使われる。
また、killの引数のプロセスIDによって意味も違うので簡単に紹介しておく。
- pid > 0 の時
指定したプロセスにシグナルを送信する - pid == 0 の時
同じプロセスグループ内のすべてのプロセスに対してシグナルを送信する - pid < -1 の時
指定されたプロセスグループIDにシグナルを送信する - pid == -1 の時
呼び出し元のプロセスに対して許可されているすべてのプロセスにシグナルを送信する
man 2 kill
などとすると詳しい動作もわかるだろう。
プロセスグループとはプロセスをまとめ、シグナルを送信したり制御を簡単にするためのもの。
プロセスグループの特徴
プロセスグループID (PGID)で管理する
プロセスグループの最初のプロセスは、プロセスグループリーダーと呼ばれ、そのプロセスID (PID) がプロセスグループID (PGID) となる
このPGIDに対してシグナルを送信するとグループ内のすべてのプロセスに対してシグナルを送信することができる
こうしてkillシステムコントロールでシグナルを送信して、失敗したら-1が返ってくるのでエラーハンドリングとして先ほどのserver側と同様の処理を行えば完成する。
シグナルを連続して送信し続けていると受信側のプロセスがシグナルを処理しきれない場合が発生する。なのでusleep関数
を使用してシグナルの送信の間隔を調整した。
終わり!!