こんにちは。
プログラミングの勉強のために筑波大の講義「システムプログラミング」をやってみようと思います。
このエントリは完全な個人のメモです。
筑波大の講義ページはこちら。
お勉強のためにこの本を読んでいるので、内容を覚えるためにSummarizing(サマライジング)を行います。
正直、講義ページがすでにかなり簡潔にまとまっているのでほぼコピペになってしまう気もしています。
第4回の講義内容は以下。
amistad06-a.hatenablog.com
シグナル
コンピュータには入出力デバイスからの通知を受け取るための割込みという仕組みがあるが、プロセスに対する割込みはシグナルという仕組みで提供される。
入出力デバイス
コンピュータはCPUとメモリとは別に、ディスプレイやマウス、キーボードなどの入出力デバイスから構成される。
プロセッサに入力を通知するために、割込みという仕組みが使われる。
CPUと各種入出力デバイスはシステムバス(データの通り道)とI/Oコントローラによって入出力の受け渡しを行う。
システムバスにはアドレスバス、データバス、制御バス(コントロールバス)がある。
入出力デバイスのIFはある程度標準化されており、マウスやキーボードではUSBが、ディスクではSATAなどがよく使用される。
一方システムバスはプロセッサ固有のものであり、入出力デバイスの一般的なIFに対応できるのはI/Oコントローラが橋渡しをしているから。
ポーリングと割込み
割込みについては以前まとめているのでそちらの記事を参照。
amistad06-a.hatenablog.com
上記の内容に補足。
ポーリング(入力/出力完了通知を待ったりすること)で入出力管理をするとCPUの使用率を著しく下げることになる。
これは入出力デバイスの処理速度がCPUの処理速度よりも圧倒的に遅いため。
割込みの仕組みを使用することによって待ち時間に他のプロセスを動かすことができるようになり、CUPを効率的に使用することができる。
割込みの際に発生するコンテキストスイッチを実現するためのプログラムを割込みハンドラや割込みサービスルーチンと呼ぶ。
同期処理と非同期処理
関数割込みは同期処理で、割込み処理は非同期処理。
割込み処理はいつどこで発生するかわからない。
割込みによってデータの一貫性が損なわれたりしないよう、プロセッサには割込みを禁止する命令もある。
例外
割込み処理とは別に例外処理がある。
例えば、アクセスできないメモリにアクセスした場合や0除算しようとした場合に起こる。
例外が発生した場合、OSカーネルの例外ハンドラが呼び出される。
シグナル
CPUとメモリはプロセスとして抽象化されているが、割込みや例外を抽象化したのがシグナル。
割込みハンドラや例外ハンドラに対応するのがシグナルハンドラ。
一般的な割込みや例外はOSカーネルでシグナルとして定義されており、プロセスはそれぞれのシグナルハンドラをOSカーネルに登録することができる。
プロセス動作中に登録してあるシグナルが届くと、登録しておいたシグナルハンドラが起動される。
シグナルを用いたプログラミング
signal(2)
シグナルをサポートするためのシステムコール。
シグナルハンドラを設定したいシグナルと、シグナルハンドラ(シグナルを受け取った時に行いたい処理を書いた関数)を引数で渡す。
戻り値は古いシグナルハンドラ。
pause(2)
signal(2)と一緒に使われるシステムコール。
pauseを呼び出したプロセスはシグナルを受け取るまで停止する。
シグナルを受け取ったらシグナルハンドラの処理を実行したのちに再開する。
signalとpauseを使用したサンプルプログラムが以下。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
// グローバル変数を定義しておく
volatile sig_atomic_t sigint_count = 3;
// シグナルハンドラの定義
void sigint_handler(int signum)
{
printf("sigint_handler: signum(%d), sigint_count(%d)\n",
signum, sigint_count);
// シグナルハンドラが呼び出されるたびにグローバル変数を減らす
// グローバル変数が0になったらプログラムを終了
if (--sigint_count <= 0) {
printf("sigint_handler: exiting ...\n");
exit(1);
}
#if 0 /* For the original System V signal */
signal(SIGINT, &sigint_handler);
#endif
}
int main(void)
{
// シグナルハンドラの登録
signal(SIGINT, &sigint_handler);
for (;;) {
printf("main: sigint_count(%d), calling pause ...\n",
sigint_count);
// シグナルを受け取るまでここで一旦プログラムを停止
pause();
printf("main: returned from pause. sigint_count(%d)\n",
sigint_count);
}
return 0;
}
プログラムの動作はプログラム内のコメントを参照。
sig_atomic
先ほどのサンプルプログラムで、グローバル変数の型として使用したsig_atomicは割り込まれずに書き込みが可能な変数の型。
変数の読み書きの途中で割り込まれることを防ぐ。
volatile
グローバル変数の修飾子として使用した。
この修飾子を付けた変数はコンパイラによって最適化されない。
また、この修飾子をつけた変数はかならずメモリに書き込まれる。
通常の変数の場合、値が更新された場合など一旦レジスタに最新の値が保持される。そのため、割込み処理などでシグナルハンドラが呼び出された際にシグナルハンドラはその値にアクセスすることができない。
常に正しい値を参照するため、この修飾子が必要。
signal(2)の問題点
■問題点①
OSによっては一度シグナルハンドラを呼び出すと元のシグナルハンドラに戻ってしまうことがある。
その場合は再設定が必要。
■問題点②
readやwriteなどのシステムコールを実行しているときにシグナルが飛んできた場合の動作もOSによって異なる。
もともと実行していたシステムコールがエラーになる場合や、自動でシステムコールを再呼び出しする場合などがある。
エラーになる場合、システムコールを使う度にシグナルを考慮しなければいけない。
■問題点③
シグナルハンドラの処理中に同じシグナルが飛んできた場合、そのシグナルハンドラは再入可能でなければならない。
シグナルハンドラ実行中にシグナルハンドラが再度呼び出されるため。
■問題点④
シグナルのブロックに関する挙動を設定できない。
sigaction(2)
signal(2)はOSによって動作が様々だったり、多くの問題があったのでPOSIXで新しく追加されたシステムコール。
POSIX環境ではsignal(2)よりもsigaction(2)を使用することが推奨される。
signal(2)の問題点は、このシステムコールの仕様では以下のようになっている。
■問題点①
シグナルハンドラはリセットされない。
■問題点②
システムコール呼び出し中にシグナルを受信した場合、デフォルトではシステムコールはキャンセルされてEINTRが返される。
引数のフラグによって再開することもできる。
■問題点③、問題点④
シグナルハンドラ実行中に同じシグナルを受信した場合、そのシグナルはいったん保留され、実行中のシグナルハンドラの処理が終わった後に再度シグナルハンドラが呼び出される。
sigaction(2)を使用したサンプルプログラムは以下。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
// グローバル変数を定義しておく
volatile sig_atomic_t sigint_count = 3;
void sigint_handler(int signum)
{
printf("sigint_handler: signum(%d), sigint_count(%d)\n",
signum, sigint_count);
// グローバル変数が0以下になったらプログラムを終了
if (--sigint_count <= 0) {
printf("sigint_handler: exiting ...\n");
exit(1);
}
}
int main(void)
{
struct sigaction sa_sigint;
// sigactionの引数の準備
// 引数の初期化
memset(&sa_sigint, 0, sizeof(sa_sigint));
// シグナルハンドラの設定
sa_sigint.sa_handler = sigint_handler;
// システムコール実行中にシグナルを受信した場合、
// シグナルハンドラ実行後にシステムコールを再開するフラグを設定
sa_sigint.sa_flags = SA_RESTART;
// sigactionでシグナルハンドラを登録
if (sigaction(SIGINT, &sa_sigint, NULL) < 0) {
perror("sigaction");
exit(1);
}
for (;;) {
printf("main: sigint_count(%d), calling pause ...\n",
sigint_count);
// シグナルを受信するまで停止
pause();
printf("main: returned from pause. sigint_count(%d)\n",
sigint_count);
}
return 0;
}
プログラムの動作はプログラム内のコメントを参照。
次のプログラムはsigactionの別の使い方の例。
上のプログラムよりもシグナルの情報を多く扱える。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
// グローバル変数を定義
volatile sig_atomic_t sigint_count = 3;
// シグナルを受信したときに行いたい動作
void sigint_action(int signum, siginfo_t *info, void *ctx)
{
printf("sigint_handler(%d): sigint_count(%d) signum(%d) code(0x%x)\n",
signum, sigint_count, info->si_signo, info->si_code);
// グローバル変数が0以下になったらプログラムを終了
if (--sigint_count <= 0) {
printf("sigint_handler: exiting ...\n");
exit(1);
}
}
int main(void)
{
struct sigaction sa_sigint;
// グローバル変数が0以下になったらプログラムを終了
// 引数の初期化
memset(&sa_sigint, 0, sizeof(sa_sigint));
// シグナルハンドラの設定
sa_sigint.sa_sigaction = sigint_action;
// システムコール実行中にシグナルを受信した場合、
// シグナルハンドラ実行後にシステムコールを再開するフラグを設定
// SA_SIGINFOフラグを立てるとsa_handlerではなく
// sa_sigactionでシグナルハンドラを指定する
// また、シグナルハンドラの引数が3つになる
// siginfo_tからより多くの情報を取得できる
// 詳細はman参照
sa_sigint.sa_flags = SA_RESTART | SA_SIGINFO;
// sigactionでシグナルハンドラを登録
if (sigaction(SIGINT, &sa_sigint, NULL) < 0) {
perror("sigaction");
exit(1);
}
while (1) {
printf("main: sigint_count(%d), calling pause ...\n",
sigint_count);
// シグナルを受信するまで停止
pause();
printf("main: returned from pause. sigint_count(%d)\n",
sigint_count);
}
return 0;
}
プログラムの動作はプログラム内のコメントを参照。
シグナルの無視
シグナルを受け取っても何もしないようにしたい場合、シグナルハンドラにSIG_IGNを指定する。
以下、サンプルプログラム。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
// グローバル変数を定義しておく
volatile sig_atomic_t sigint_count = 3;
int main(void)
{
struct sigaction sa_ignore;
// sigactionの引数の準備
// 引数の初期化
memset(&sa_ignore, 0, sizeof(sa_ignore));
// シグナルハンドラにSIG_IGNを指定
// (シグナルを受け取っても何もしない)
sa_ignore.sa_handler = SIG_IGN;
// sigactionでシグナルハンドラを登録
if (sigaction(SIGINT, &sa_ignore, NULL) < 0) {
perror("sigaction");
exit(1);
}
while (1) {
printf("main: sigint_count(%d), calling pause ...\n",
sigint_count);
// シグナルを受信するまで停止
pause();
printf("main: returned from pause. sigint_count(%d)\n",
sigint_count);
}
return 0;
}
Ctrl+Cしてもシグナルは無視されるため、プログラムは停止しない。
その他のプログラムの動作はプログラム内のコメントを参照。
kill(2)
プロセスにシグナルを送るためのシステムコール。
シグナルの送り先のプロセスと送るシグナルの種類は引数で指定する。
以下、kill(2)を使ったサンプルプログラム。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int main(int argc, char *argv[])
{
pid_t pid;
if (argc != 2) {
fprintf(stderr, "Usage: %s pid\n", argv[0]);
exit(1);
}
// 文字列->数値への変換
pid = atoi(argv[1]);
// 引数で渡されたPIDが0以下ならプログラムを終了
if (pid <= 0) {
fprintf(stderr, "Invalid pid: %d\n", pid);
exit(1);
}
// 引数で渡されたIDのプロセスにSIGINTを送る
if (kill(pid, SIGINT) < 0) {
perror("kill");
exit(1);
}
return 0;
}
プログラムの動作はプログラム内のコメントを参照。
インターバルタイマ
一定周期でシグナルを発生させたい場合にはインターバルタイマという仕組みを使用する。
インターバルタイマを使用するためにはsetitimer(2)というシステムコールを使用する。
以下のプログラムはsetitimer(2)のサンプルプログラム。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <sys/time.h>
// グローバル変数を定義
volatile sig_atomic_t alrm_count = 10;
// シグナルを受信したときに起動する関数
void alrm(int signum)
{
// グローバル変数を減らす
alrm_count--;
}
int main(void)
{
struct sigaction sa_alarm;
struct itimerval itimer;
// sigactionの引数の準備
// 引数の初期化
memset(&sa_alarm, 0, sizeof(sa_alarm));
// シグナルハンドラの設定
sa_alarm.sa_handler = alrm;
// システムコール実行中にシグナルを受信した場合、
// シグナルハンドラ実行後にシステムコールを再開するフラグを設定
sa_alarm.sa_flags = SA_RESTART;
// sigactionでシグナルハンドラを登録
if (sigaction(SIGALRM, &sa_alarm, NULL) < 0) {
perror("sigaction");
exit(1);
}
// インターバルタイマの間隔を1sec0msecに設定
itimer.it_value.tv_sec = itimer.it_interval.tv_sec = 1;
itimer.it_value.tv_usec = itimer.it_interval.tv_usec = 0;
// インターバルタイマをセット
// ここからタイマのカウントが開始され、
// 実時間の1sec0msrc周期でSIGALRMが送信される
if (setitimer(ITIMER_REAL, &itimer, NULL) < 0) {
perror("setitimer");
exit(1);
}
while (alrm_count) {
pause();
printf("%d: %ld\n", alrm_count, time(NULL));
}
// 10回シグナルを受け取ったらタイマを無効にする
itimer.it_value.tv_sec = itimer.it_interval.tv_sec = 0;
itimer.it_value.tv_usec = itimer.it_interval.tv_usec = 0;
if (setitimer(ITIMER_REAL, &itimer, NULL) < 0) {
perror("setitimer");
exit(1);
}
return 0;
}
プログラムの動作はプログラム内のコメントを参照。
マニュアルによると、setitimer(2)は今後廃止予定で、代わりにtimer_settime(2)やtimer_gettime(2)を使用することを推奨している。
まとめ
第5回の講義内容は以上です。
正直私のメモ見るよりも講義ページを見るのが良いと思います。
非常に簡潔にまとまっていてわかりやすいです。
第6回の講義の内容はこちら。
amistad06-a.hatenablog.com
おわり。