Ctrl+Cとkill -SIGINTの違いからLinuxプロセスグループを理解する
しばらくLinuxネタが続く・・。
近いうちに最近出たJava8ネタを書いてみようと思います。が、もう少しLinuxネタにお付き合いください。
前回はsshdを対象に親プロセスをkillした場合の動作を確認した。
killされたプロセスの子プロセスは孤児プロセスとなり、カーネルによって自動的にinitプロセスの子として扱われる事を説明した。(この動作を「リペアレンティング」と呼ぶ)
今回はこの続き。
Linuxで作業していてCtrl+Cしてプロセスを終了した場合、フォアグラウンドのプロセスやその子プロセスも一緒に終了する。
ということは、子プロセスは孤児として扱われず、リペアレンティングされていないことになる。
今回の記事ではこの振る舞いの違い(リペアレンティングされるか否か)に着目し、kill -SIGINTコマンドとCtrl+Cの違いについて考えていく。
そもそもkillコマンドやCtrl+Cは何をしているのか?
プロセスに対してシグナルを送っている。
シグナルとはプロセス間通信の一種で、他のプロセスに対してイベントを通知することができる仕組みだ。
シグナルを受け取ったプロセスは現在実行中の処理を中断し、シグナルに応じた処理を実行する。
killコマンドは特定のプロセスにシグナルを送ることができるし、Ctrl+Cはフォアグラウンドで起動中のプロセスに対するシグナル送信を行う。
やり方は違っても、killや、Ctrl+Cはプロセスに対してシグナルを送っている点で同様なのだ。
以下はよく使うシグナルの一覧だ。
シグナル | killでの送り方 | Ctrl+X での送り方 | シグナルの内容 |
---|---|---|---|
SIGHUP | kill -SIGHUP <プロセスID> kill -HUP <プロセスID> kill -1 <プロセスID> | - | ハングアップ (端末が切断された場合 などに発生) |
SIGINT | kill -SIGINT <プロセスID> kill -INT <プロセスID> kill -2 <プロセスID> | Ctrl+C | 割り込み(Interrupt)の発生。 |
SIGKILL | kill -SIGKILL <プロセスID> kill -KILL <プロセスID> kill -9 <プロセスID> | - | プロセスの強制終了 |
SIGTERM | kill <プロセスID> kill -SIGTERM <プロセスID> kill -TERM <プロセスID> kill -15 <プロセスID> | - | プロセスの終了 (killコマンドデフォルトシグナル) |
SIGTSTP | kill -SIGTSTP <プロセスID> kill -TSTP <プロセスID> | Ctrl+Z | プロセスの一時停止 (フォアグラウンドプロセスを バックグラウンドに 移す場合によく使う) |
シグナル受信時のプロセスの振る舞い
注意が必要なのは、シグナル受信時の振る舞いは個々のプロセスの実装によるということだ。
例えばSIGINTシグナルを受け取ったからといって必ずプロセスが終了するかというとそうではない。
シグナル受信時の振る舞いはプロセスの実装次第であり、”プロセスが終了する”というのはあくまでデフォルトの動作であって、実装指針でしかない。
ただし、SIGKILLは特別
SIGKILLシグナルだけは例外で、実装に依存しない。
外部から強制的にプロセスを終了するため、個々のプロセスでこの振る舞いを変えることはできない。
プロセス終了の最終手段な訳だが、正常な終了処理が実行されないため、実行以後予期せぬトラブルを招く可能性がある。ということで、SIGKILLのご利用は計画的に!
(そもそもSIGKILLの利用を計画している時点で間違いだが・・・)
問題の動作を確認!プロセスをforkしてkill!
それでは問題の振る舞いについて確認してみよう。
検証に使うコード
今回は検証用のコードとして以下の簡単なRubyコードを利用する。
#!/usr/bin/ruby fork { #子プロセスIDの表示 puts "child process id is #{Process.pid}" #勝手に終了しないようにスリープ sleep } #親プロセスIDの表示 puts "parent process id is #{Process.pid}" #勝手に終了しないようにスリープ sleep
rubyのforkメソッドはプロセスをforkし、その子プロセスにブロック内のコードを実行させる。
また、ブロック内の処理終了と同時に子プロセスを解放する。
上記のコードは4〜8行目は子プロセスによってのみ実行され、10行目以降は親プロセスによってのみ実行されることになる。
試しにこのコードを実行すると
$ ruby process-signal-test.rb & parent process id is 5662 child process id is 5663 $ pstree -p #抜粋 init(1)─┬ ├─login(1461) └──bash(3013) └──ruby(5662) └──ruby(5663)
となり、rubyプロセスに親子関係ができていることが分かる。
親、子プロセスともsleepしているので、待機している状態だ。
これで好き放題シグナルが送れる!
やってみよう!
では、Rubyの親プロセスに対してkillとCtrl+Cを実行し、それぞれの振る舞いを比較してみよう。
Ctrl+Cで送るシグナルはSIGINTなので、killコマンドはオプションに-SIGINTを付けて比較する。
Ctrl+Cする場合
Ruby検証用コードをフォアグラウンドで起動させ、Ctrl+Cしてみる。
$ ruby process-signal-test.rb parent process id is 5772 child process id is 5723 # ここでCtrl+Cする ^Cprocess-signal-test.rb:10:in `sleep': Interrupt from process-signal-test.rb:10 $ process-signal-test.rb:5:in `sleep': Interrupt from process-signal-test.rb:5 from process-signal-test.rb:3:in `fork' from process-signal-test.rb:3 $ pstree -p | grep ruby $ #rubyプロセスなし
Ctrl+Cした後、rubyのプロセスは残っていなかった。
つまり、Ctrl+Cすることによって、親子両方のプロセスが消滅した。
kill -SIGINTする場合
Ruby検証用コードをバックグラウンドで起動し、killコマンドでシグナルを送ってみる。
$ ruby process-signal-test.rb & parent process id is 5763 child process id is 5764 $ kill -SIGINT 5763 #親プロセスにSIGINTを送信 process-signal-test.rb:15:in `sleep': Interrupt from process-signal-test.rb:15 [1]+ 割り込み ruby process-signal-test.rb $ pstree -p #抜粋 init(1)─┬ ├─ruby(5764)
子プロセス(5764)が生き残り、親プロセスがinitになった。
つまり、子プロセスは孤児プロセスとして扱われ、リペアレンティングされた。
結果の比較
Ctrl+Cした場合は親子プロセスが両方とも消滅し、kill -SIGINTした場合は子プロセスのみ生き残った。
どちらのコマンドも親プロセスに対してSIGINTシグナルを送っている点は同じである。
何故このような振る舞いの違いが生まれたのだろうか?
プロセスグループと端末(tty/pts)の仕様
この振る舞いはプロセスグループと端末(tty/pts)の仕様が関係している。
先ず、プロセスグループと、端末(tty/pts)について説明する。
プロセスグループとは??
プロセスグループとはLinux/Unixにおけるプロセス管理の仕組みであり、複数のプロセスを束ねた集合のことである。通常、プロセスをforkして子プロセスを作成した場合、親、子プロセスは同一プロセスグループに所属する。
子がさらに子をforkしてもこれらはすべて同一の親にぶら下がる一つのグループとして扱われる。
このプロセスグループはpsコマンドで確認できる。
$ ruby process-signal-test.rb & parent process id is 5909 child process id is 5910 $ ps -eo pid,pgid,command | egrep "COMMAND|ruby" PID PGID COMMAND 5909 5909 ruby process-signal-test.rb 5910 5909 ruby process-signal-test.rb 5912 5911 egrep COMMAND|ruby
psコマンド実行結果のPGID列を見て欲しい。
今回rubyプロセス5909、5910は双方ともPGID列が5909となっている。
これはこれらのプロセスが同一のプロセスグループ「5909」に所属していることを表している。
通常、プロセスグループIDは親プロセスのプロセスIDと同様の値が割り当てられる。
これらの2プロセスは、カーネル上同一グループに属するプロセスとして取り扱われる。
端末って??tty?pts?
次に端末やttyやptsについて解説する。
端末とは?
端末とはLinuxに対する標準入力や標準出力をLinuxと仲介する役割を担っている。
例えば、キーボードで入力した文字列を標準入力としてLinux/Unixに渡す、標準出力をコンソール上に表示するなどといった動作だ。
そして、ttyやptsは両方とも端末の事を指している。
ttyとptsの違いは?
Linuxに直接接続している場合に使われる端末はttyであり、telnetやsshなど、遠隔地から接続している場合に利用される端末はptsである。
現在利用している端末は「tty」コマンドで確認できる。
sshで接続している場合は
$ tty /dev/pts/1
と表示され、直接接続している場合は
$ tty /dev/tty1
のように表示される。
遠隔地から接続する場合はpts、直接接続はttyを経由していることが分かる。
端末が分かればこんなイタズラもできる
ちなみに、標準入力や標準出力を仲介する役割があるということは、次のようなイタズラもできる。
例えば、「/dev/pts/1」「/dev/pts/2」を経由し、2ユーザがLinuxに接続しているとする。
このファイルに標準出力をリダイレクトすることで、相手の端末上に文字列を表示できる。
$ tty /dev/pts/1 $ sudo echo "もうすぐシャットダウンする!早く接続を切れ!間に合わなくなってもしらんぞーーーっ!!!" > /dev/pts/2 $
とすると「/dev/pts/2」側では
$ tty /dev/pts/2 $ もうすぐシャットダウンする!早く接続を切れ!間に合わなくなってもしらんぞーーーっ!!!
と勝手に表示され、pts2ユーザはいきなりベジータからのシャットダウン警告を受けることにってしまう訳だ。
さすがにこれは接続を切らざるを得ないだろう。
Ctrl+Cの正体
ここまで来れば、Ctrl+Cの正体まであと一歩だ。
端末は標準入力を受け付け、Linuxに渡す役割を持っていた。
つまり、キーボードでCtrl+Cを入力した際、このCtrl+C入力情報は端末に受け渡され、対応するシグナルに変換されてプロセスに通知される。
この”何を入力した時に何に変換するのか?”は、「stty -a」コマンドで確認できる。
$ stty -a speed 9600 baud; rows 47; columns 90; line = 0; intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = M-^?; eol2 = M-^?; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0; -parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts -cdtrdsr -ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc ixany imaxbel -iutf8 opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 isig icanon iexten echo echoe -echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke
この結果ではCtrl+C(^C)に対して「intr」が割り当てられている。(3行目の先頭)
つまり、端末はCtrl+Cを受け取った場合、SIGINTシグナルをプロセスに送っていることが分かる。
まとめ – Ctrl+Cとkill -SIGINTの違い
以上の内容を元に、違いをまとめる。
どちらもプロセスにSIGINTシグナルを送っている点は同じだ。
- Ctrl+C ⇒ 端末がCtrl+CをSIGINTシグナルに変換しプロセスに送信
- kill -SIGINT ⇒ 指定されたプロセスにSIGINTシグナルを送信
やっていることは二つとも同じだが、異なる点が一カ所だけある。
それは”シグナルを送信するプロセス”だ。
Ctrl+Cとkill -SIGINTのシグナル送信先をまとめると以下の通りである。
コマンド | シグナル送信先のプロセス |
---|---|
kill -SIGINT | 指定されたプロセスにのみシグナルを送信 (プロセスの親子は関係ない) |
Ctrl+C | フォアグラウンドで動作するプロセスのプロセスグループに属する 全プロセスに対して送信 |
killコマンドは特定のプロセスにのみ送信するのに対し、Ctrl+Cはプロセスグループ全体にシグナルを送信している。
以下のようなイメージとなる。
つまり、kill -SIGINTもCtrl+CもSIGINTシグナルを送信している点は同じであるが、送っているプロセスの範囲が異なるという訳だ。
killした場合は特定のプロセスに対してのみシグナルが送られるのに対し、Ctrl+Cした場合は、入力情報が端末でSIGINTに変換され、プロセスグループ全体に送信される。これは端末の仕様だ。
これをrubyの実行結果で考えてみると、Ctrl+Cした場合、親rubyプロセスにぶら下がる子プロセスはすべて終了しているため、そもそもリペアレンティング対象となるプロセスが存在しない状態となっていた訳だ。
逆にkill -SIGINTした場合は子プロセスは生き残っているから、カーネルによって子プロセスのリペアレンティングが行われた。
これが前述の振る舞いの違いの正体だ。
今回は随分長丁場になった。
Linux/Unixのプロセス管理の考え方としてプロセスグループとは別に「セッション」という考え方がある。
セッションはプロセスグループよりも更に範囲を広くしたプロセス管理のグルーピングのことだ。
どういう場面で役立っているのか、近々まとめてみようと思う。
関連記事
-
Linuxプロセス起動時の環境変数ダンプの取得
UnixやLinux上で不具合の調査等々を行う際、特定のプロセス起動時の環境変数を知りたい場合がある
-
Java8のHotSpotVMからPermanent領域が消えた理由とその影響
今回も前回の記事につづき、Java8による変更点で未だあまり紹介されていないポイントを記事にしようと
-
Java8のインタフェース実装から多重継承とMixinを考える
2014年3月18日、ついにJava8が正式にリリースを迎えた。 折角なので、今後、Java8の新
-
文字コードの考え方から理解するUnicodeとUTF-8の違い
UnicodeとUTF-8の違いを理解していない方が結構居るようなので、文字コードの考え方を元に解説
-
ipsetを使ってスマートにiptablesを設定する
ギークな知人から「vpsでiptables設定していたらルール設定数の上限に引っかかって思い通りの設
-
「Systemd」を理解する ーシステム管理編ー
前回の記事「Systemd」を理解するーシステム起動編ーでは、Systemdの概念とSystemdに
-
例示専用のIPアドレスとドメインを使いこなす
前回の記事ではネットワークに関する記事を投稿させていただいたが、今回も引き続きネットワーク関連のネタ
-
sshd再起動時にssh接続が継続する動作について
Linux/Unixサーバにsshしている際、sshdを再起動したとする。 sshdは一度終了する
-
「Systemd」を理解する ーシステム起動編ー
2014年6月10日、とうとうRHEL7が正式リリースを迎えた。RHEL7での変更点については、この
Comment
[…] Ctrl+Cとkill -SIGINTの違いからLinuxプロセスグループを理解する | ギークを目指して:メモ。 […]
[…] Ctrl+Cとkill -SIGINTの違いからLinuxプロセスグループを理解する | ギークを目指して […]
[…] Ctrl+Cとkill -SIGINTの違いからLinuxプロセスグループを理解する | ギークを目指して […]
[…] Ctrl+Cとkill -SIGINTの違いからLinuxプロセスグループを理解する | ギークを目指して […]
sudo echo “…” > /dev/pts/2
の sudo は結果に影響を与えないような気がしました。
匿名様
コメントありがとうございます。
ご指摘の通り、「sudo echo “…” > /dev/pts/2」の部分は私の確認が不足していたようです。
この点をクリアにするために以下の2点を補足させていただきます。
補足1.tty/ptsファイルの所有者、グループ、およびパーミッション
tty/ptsデバイスファイルは以下のルールで所有者、グループとパーミッションが設定されます。
(検証環境はCentOS6.5、およびCentOS7です)
所有者:接続したユーザ
グループ:tty
パーミッション:crw–w—-
パーミッションを見ると所有者による読み込みと書き込みを許可している一方で、グループは書き込みのみ、その他ユーザは権限が一切与えられていないことが分かります。
そのため、ttyにリダイレクトしてイタズラしたい場合、リダイレクト先ttyを利用しているユーザが、”同一ユーザ”、もしくは”同一グループ”でない限り、リダイレクトが権限違反により失敗してしまいます。
⇒ これを実施するためには、”特権でリダイレクトを実施する”か、”tty/ptsのパーミッションを変更する”必要があります。今回紹介した例では前者の方法を用いましたが、以下「補足2」に記載する理由で、実はこれでもうまくいきません。
補足2.sudoでリダイレクトした際に適用される権限
sudoを利用した場合であっても、リダイレクトは実行元のユーザで実行(権限が戻ってしまう)されてしまいます。(この辺はsudoのmanに記載されています)
そのため、tty/ptsへ文字列をリダイレクトできません。リダイレクトも含めて特権を適用したい場合はサブシェルを利用する必要があります。
ということで、次のコマンドを利用することで別ユーザへのtty/ptsによるイタズラを実行できるでしょう。
コマンド:
sudo sh -c "echo message > /dev/pts/x"
kill(1) のマニュアル読めばわかりますが(すでに気付いているかもしれませんが)、`kill -INT -` でプロセスグループにシグナル送れますよ。
とても分かりやすい解説でした。
参考にさせていただきました。
https://qiita.com/_kazuya/items/883fbcdb66cf4b51c8b1#_reference-49c7502a9b8248879683
とても分かりやすい解説でした。
参考にさせていただきました。
https://qiita.com/_kazuya/items/883fbcdb66cf4b51c8b1#_reference-49c7502a9b8248879683
vyc9jg
b1niy0
oivfbd
apxc65
czc3c6