*

Ctrl+Cとkill -SIGINTの違いからLinuxプロセスグループを理解する

公開日: : 最終更新日:2014/05/18 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
での送り方
シグナルの内容
SIGHUPkill -SIGHUP <プロセスID>
kill -HUP <プロセスID>
kill -1 <プロセスID>
-ハングアップ
(端末が切断された場合
などに発生)
SIGINTkill -SIGINT <プロセスID>
kill -INT <プロセスID>
kill -2 <プロセスID>
Ctrl+C割り込み(Interrupt)の発生。
SIGKILLkill -SIGKILL <プロセスID>
kill -KILL <プロセスID>
kill -9 <プロセスID>
-プロセスの強制終了
SIGTERMkill <プロセスID>
kill -SIGTERM <プロセスID>
kill -TERM <プロセスID>
kill -15 <プロセスID>
-プロセスの終了
(killコマンドデフォルトシグナル)
SIGTSTPkill -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と同様の値が割り当てられる。

processgroup

これらの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はプロセスグループ全体にシグナルを送信している。
以下のようなイメージとなる。

differentOfCtrlCAndKill-1

つまり、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

  1. […] Ctrl+Cとkill -SIGINTの違いからLinuxプロセスグループを理解する | ギークを目指して:メモ。 […]

  2. […] Ctrl+Cとkill -SIGINTの違いからLinuxプロセスグループを理解する | ギークを目指して […]

  3. […] Ctrl+Cとkill -SIGINTの違いからLinuxプロセスグループを理解する | ギークを目指して […]

  4. 匿名 より:

    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"

  5. fumiyas より:

    kill(1) のマニュアル読めばわかりますが(すでに気付いているかもしれませんが)、`kill -INT -` でプロセスグループにシグナル送れますよ。

  6. 匿名 より:

    とても分かりやすい解説でした。
    参考にさせていただきました。
    https://qiita.com/_kazuya/items/883fbcdb66cf4b51c8b1#_reference-49c7502a9b8248879683

  7. odenn より:

    とても分かりやすい解説でした。
    参考にさせていただきました。
    https://qiita.com/_kazuya/items/883fbcdb66cf4b51c8b1#_reference-49c7502a9b8248879683

Message

メールアドレスが公開されることはありません。

「Systemd」を理解する ーシステム管理編ー

前回の記事「Systemd」を理解するーシステム起動編ーでは、Syst

「Systemd」を理解する ーシステム起動編ー

2014年6月10日、とうとうRHEL7が正式リリースを迎えた。RHE

例示専用のIPアドレスとドメインを使いこなす

前回の記事ではネットワークに関する記事を投稿させていただいたが、今回も

ipsetを使ってスマートにiptablesを設定する

ギークな知人から「vpsでiptables設定していたらルール設定数の

Java8のHotSpotVMからPermanent領域が消えた理由とその影響

今回も前回の記事につづき、Java8による変更点で未だあまり紹介されて

→もっと見る

PAGE TOP ↑