# シグナルってなに? - シェルもどきをgoで自作する#19 ## おさらい これまで - [シェルってなに?コマンドラインインタプリタってなに? - シェルもどきをgoで自作する#1](https://hackmd.io/@jyami/HJzohRn2D) - [コマンドと引数の分解、環境変数PATHから探索、外部コマンドと内部コマンド - シェルもどきをgoで自作する #2](https://hackmd.io/@jyami/HyeSkkThP) - [字句解析その1 - シェルもどきをgoで自作する #3](https://hackmd.io/@jyami/Hk3bWSMQO) - [字句解析その2 - シェルもどきをgoで自作する #4](https://hackmd.io/@jyami/S1BkltxQu) - [リダイレクションってなに? - シェルもどきをgoで自作する #5](https://hackmd.io/@jyami/S113NQx8u) - [リダイレクションの種類 - シェルもどきをgoで自作する #6](https://hackmd.io/@jyami/rJ3XmClqd) - [リダイレクションの構文 - シェルもどきをgoで自作する #7](https://hackmd.io/@jyami/BJ04J2Upd) - [コマンドプロセスを作成する際のファイルディスクリプタの操作 - シェルもどきをgoで自作する #8](https://hackmd.io/@jyami/Hy7nSciMt) - [構文解析と抽象構文木 - シェルもどきをgoで自作する #9](https://hackmd.io/@jyami/ByrK1ajIK) - [パイプってなに? - シェルもどきをgoで自作する #10](https://hackmd.io/@jyami/SkXR3iltK) - [ワイルドカードってなに? - シェルもどきをgoで自作する #11](https://hackmd.io/@jyami/SJghKSl3F) - [環境変数ってなに? - シェルもどきをgoで自作する #12](https://hackmd.io/@jyami/H12wYg4L9) - [環境変数の設定と環境変数の展開 - シェルもどきをgoで自作する #13](https://hackmd.io/@jyami/Hy3EefRgj) - [シェル変数との違いからみた環境変数 - シェルもどきをgoで自作する #14](https://hackmd.io/@jyami/ByxFrm6Si) - [シェル変数を実装 - シェルもどきをgoで自作する #15](https://hackmd.io/@jyami/rJf5GFRhs) - [プロセスグループ/セッションってなに? - シェルもどきをgoで自作する #16](https://hackmd.io/@jyami/Hk0J0E7vn) - [ジョブってなに? - シェルもどきをgoで自作する#17](https://hackmd.io/@jyami/Hyej-Ncoh) - [ジョブ制御ってなに? - シェルもどきをgoで自作する#18](https://hackmd.io/@jyami/HyIJ9qUe6) シェルもどき「[oreshell](https://github.com/jyami/oreshell)」を自作している。 前回は、ジョブ制御とは何かについて調べた。また、ジョブ制御には「シグナル」を使っていることがわかった。 今回はシグナルについて調べる。 ## シグナルとは > シグナル(英: signal)とは、Unix系(POSIX標準に類似の)オペレーティングシステム (OS) における、限定的なプロセス間通信であり、プロセスに対し非同期でイベントの発生を伝える機構である。シグナルが送信された際、OSは宛先プロセスの正常な処理の流れに割り込む。どんな不可分でない処理の間でも割り込むことができる。受信プロセスが以前にシグナルハンドラを登録しておけば、シグナル受信時にそのルーチンが実行される。さもなくば、デフォルトのシグナル処理が行われる。[wikipediaより](https://ja.wikipedia.org/wiki/%E3%82%B7%E3%82%B0%E3%83%8A%E3%83%AB_(Unix)) 「シグナル」はプロセス間で通信を行うための信号である。 シグナルを受け取ったプロセスはプロセス自身が持つ「表」に従ってその動作を決定する。 ![signal-ページ1.drawio](https://hackmd.io/_uploads/rk5dhVD86.png) ## シグナルを送る例 以下のような永久ループするプログラム(go言語)があったとする。 ```signal_samle.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 ``` 永久ループしているプロセスが停止する。 停止までの処理を図にすると以下の通り。 ![signal-ページ2.drawio (1)](https://hackmd.io/_uploads/rJ6tnNwLp.png) プロセスIDのみ指定してkillコマンドを実行すると、そのプロセスに対してシグナル「SIGTERM」を送る。 プロセスはSIGTERMを受け取ると、表からそれを探し出し、デフォルトアクション「プロセス終了」を実行する。 #### ■余談 killコマンドはその名の通りプロセスを殺すためによく使われるが、プロセス終了(SIGTERM)以外のシグナルを送ることもできる。以下は例。 ```a.sh $ kill -STOP <PID> $ kill -CONT <PID> ``` ### 端末から「Ctrl+C」キーを押下する 先ほどと同様に永久ループするプログラムを実行し、端末から「Ctrl+C」キーを押下する。 ``` ... 永久ループ 永久ループ ^C ``` 永久ループしているプロセスが停止する。 停止までの処理を図にすると以下の通り。 ![signal-ページ4.drawio (2)](https://hackmd.io/_uploads/S1NJ6NvU6.png) 端末は、特定のキーとそれに対応する特殊制御文字を一覧にした表を持つ。 特殊制御文字は、端末に結び付いたフォアグラウンドプロセスグループやその標準入力を制御するためのものである。 端末に特定のキーを入力すると表に従って特殊制御文字の処理を実行する。 その表は「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」を送る。([参考](https://www.gnu.org/software/libc/manual/html_node/Signal-Characters.html)) プロセスはSIGINTを受け取ると、表からそれを探し出し、デフォルトアクション「プロセス終了」を実行する。 ## シグナルハンドラ(ユーザ定義アクション) プロセスのシグナル受信時のアクションとしてユーザが定義したアクションを、プログラム中で割り当てることができる。 go言語によるサンプルプログラムで例を示す。 ```signal_samle.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)](https://hackmd.io/_uploads/HkmOaNDIp.png) プログラム中で任意のシグナル(ここでは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)](https://hackmd.io/_uploads/HJ2B_SDUa.png) シグナル「SIGTERM」についてはユーザ定義アクションを指定していないためデフォルトアクション(プロセス終了)を実行した。 ## 次回 次回からoreshellのジョブ制御の実装を進める。