Try   HackMD

シグナルってなに? - シェルもどきをgoで自作する#19

おさらい

これまで

シェルもどき「oreshell」を自作している。
前回は、ジョブ制御とは何かについて調べた。また、ジョブ制御には「シグナル」を使っていることがわかった。
今回はシグナルについて調べる。

シグナルとは

シグナル(英: signal)とは、Unix系(POSIX標準に類似の)オペレーティングシステム (OS) における、限定的なプロセス間通信であり、プロセスに対し非同期でイベントの発生を伝える機構である。シグナルが送信された際、OSは宛先プロセスの正常な処理の流れに割り込む。どんな不可分でない処理の間でも割り込むことができる。受信プロセスが以前にシグナルハンドラを登録しておけば、シグナル受信時にそのルーチンが実行される。さもなくば、デフォルトのシグナル処理が行われる。wikipediaより

「シグナル」はプロセス間で通信を行うための信号である。
シグナルを受け取ったプロセスはプロセス自身が持つ「表」に従ってその動作を決定する。

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

シグナルを送る例

以下のような永久ループするプログラム(go言語)があったとする。

func main() {
	for { // 永久ループ
		select {
		case <-time.After(time.Duration(1 * time.Second)):
			fmt.Println("永久ループ")
		}
	}
}

実行すると

$ ./signal_sample
永久ループ
永久ループ
永久ループ
...

永久ループするプログラムなので当然ながら、延々と実行を続ける。
このプロセスを外部から停止したいとする。
いろいろ方法があるが、ここでは以下の二つの方法を行う。

  • killコマンドを実行する
  • 端末から「Ctrl+C」キーを押下する

killコマンドを実行する

別の端末で、永久ループしているプロセスのIDを調べる

$ ps a | grep signal_sample
16354 pts/10   Sl+    0:00 ./signal_sample
16432 pts/9    S+     0:00 grep --color=auto signal_sample

永久ループしているプロセスIDを指定してkillコマンドを実行する。

$ kill 16354

そうすると

...
永久ループ
永久ループ
Terminated

永久ループしているプロセスが停止する。
停止までの処理を図にすると以下の通り。

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

プロセスIDのみ指定してkillコマンドを実行すると、そのプロセスに対してシグナル「SIGTERM」を送る。
プロセスはSIGTERMを受け取ると、表からそれを探し出し、デフォルトアクション「プロセス終了」を実行する。

■余談

killコマンドはその名の通りプロセスを殺すためによく使われるが、プロセス終了(SIGTERM)以外のシグナルを送ることもできる。以下は例。

$ kill -STOP <PID>
$ kill -CONT <PID>

端末から「Ctrl+C」キーを押下する

先ほどと同様に永久ループするプログラムを実行し、端末から「Ctrl+C」キーを押下する。

...
永久ループ
永久ループ
^C

永久ループしているプロセスが停止する。
停止までの処理を図にすると以下の通り。

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

端末は、特定のキーとそれに対応する特殊制御文字を一覧にした表を持つ。
特殊制御文字は、端末に結び付いたフォアグラウンドプロセスグループやその標準入力を制御するためのものである。
端末に特定のキーを入力すると表に従って特殊制御文字の処理を実行する。
その表は「stty」コマンドで確認できる。

$ stty -a
speed 38400 baud; rows 29; columns 120; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q;
stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; discard = ^O; min = 1; time = 0;
(略)

通常、「Ctrl+C」キーには特殊制御文字「INTR(割り込み)」が割り当てられる。
INTRは、端末に関連付けられたフォアグラウンドプロセスグループの全プロセスに対してシグナル「SIGINT」を送る。(参考)
プロセスはSIGINTを受け取ると、表からそれを探し出し、デフォルトアクション「プロセス終了」を実行する。

シグナルハンドラ(ユーザ定義アクション)

プロセスのシグナル受信時のアクションとしてユーザが定義したアクションを、プログラム中で割り当てることができる。

go言語によるサンプルプログラムで例を示す。

func main() {

	sigs := make(chan os.Signal, 1)
	// SIGINTについてユーザ定義アクションする
	signal.Notify(sigs, syscall.SIGINT)

	// シグナルを受け取るためgoroutine
	go func() {
		for {
			select {
			case sig := <-sigs: // ここでシグナルを待つ
				fmt.Printf("%v を受け付けた\n", sig)
				fmt.Printf("ここでなんらかのアクションを実行する\n")
			}
			// 終了せずにまたシグナルを待つ
		}
	}()

	for { // 永久ループ
		select {
		case <-time.After(time.Duration(1 * time.Second)):
			fmt.Println("永久ループ")
		}
	}
}

実行すると

$ ./signal_sample
永久ループ
永久ループ
永久ループ
...

延々と実行を続ける。
端末から「Ctrl+C」キーを押下すると

...
永久ループ
永久ループ
^Cinterrupt を受け付けた
ここでなんらかのアクションを実行する
永久ループ
永久ループ
...

前回と違いプロセスを終了しない。
代わりに
「interrupt を受け付けた」
「ここでなんらかのアクションを実行する」
を表示した。

signal-ページ5.drawio (1)

プログラム中で任意のシグナル(ここではSIGINT)についてユーザ定義アクションを設定すると、シグナル受信時にデフォルトアクションではなくユーザ定義アクションを実行する。(ただし、SIGKILLとSIGSTOPは指定不可)
よって、signal_sampleプロセスはプロセス終了をせずにユーザ定義アクションを実行し、そのあと永久ループを続けた。

この状態でSIGTERMを送ると?

先ほどと同じように、別の端末で永久ループしているプロセスのIDを調べ、永久ループしているプロセスIDを指定してkillコマンドを実行する(シグナル「SIGTERM」を送る)。

$ ps a | grep signal_sample
 5579 pts/2    Sl+    0:00 ./signal_sample
 5616 pts/6    S+     0:00 grep --color=auto signal_sample
$ kill 5579

そうすると

...
永久ループ
永久ループ
Terminated

前回と同じく、永久ループしているプロセスが停止する。

signal-ページ6.drawio (2)

シグナル「SIGTERM」についてはユーザ定義アクションを指定していないためデフォルトアクション(プロセス終了)を実行した。

次回

次回からoreshellのジョブ制御の実装を進める。