第7章 プロセス管理
実行中のプログラムのことをプロセスと呼びます。この章では、動いているプロセスを確認し、必要に応じて止める方法を学びます。
ps — 実行中のプロセスを表示
$ ps aux
USER PID %CPU %MEM COMMAND
user 1 0.0 0.1 /sbin/init
user 482 0.3 1.2 nginx
user 903 0.0 0.5 bash
各プロセスにはPID(プロセスID)という一意の番号が割り振られます。プロセスを操作するときはこの番号を使います。
| オプション | 意味 |
|---|---|
aux | 全ユーザーの全プロセスを、CPU・メモリ使用率つきで表示(BSD形式) |
-ef | 全ユーザーの全プロセスを、親プロセスID(PPID)つきで表示(UNIX形式) |
ps aux の各項目の意味
| 項目 | 意味 |
|---|---|
| USER | そのプロセスを実行しているユーザー |
| PID | プロセスID |
| %CPU | CPU使用率 |
| %MEM | メモリ使用率 |
| STAT | プロセスの状態(実機の表示項目。R=実行中、S=待機中、Z=ゾンビ、T=停止中 など) |
| COMMAND | 実行されたコマンド(プログラム)名 |
この表のうち本章の説明で使う出力例では USER・PID・%CPU・%MEM・COMMAND の5項目に絞っていますが、実機の ps aux ではこれ以外にも起動からの経過時間(TIME)や使用メモリ量(RSS)など、より多くの項目が表示されます。慣れないうちはすべてを追う必要はなく、まずはPIDとCOMMANDの2つを見られれば十分です。
親プロセスと子プロセス
Linuxでは、あるプロセスが新しいプロセスを生成すると、生成した側を親プロセス、生成された側を子プロセスと呼びます。たとえばターミナルで ps を実行すると、bash(シェル)が親プロセスとなってpsという子プロセスを生成しています。すべてのプロセスをたどっていくと、最終的にはPID 1(多くのディストリビューションではinitやsystemd)という、システム起動時に最初に生まれるプロセスに行き着きます。親プロセスが終了すると、その子プロセスも道連れで終了することがある点も覚えておくとよいでしょう。
実機の ps -ef や ps -ejH を使うと、PPID(親プロセスID)や親子関係のツリー構造を確認できます。あるプロセスが誰から起動されたのかを調べたいときに役立つので、余裕があれば試してみてください。
子プロセスが終了しても、親プロセスがその終了ステータスを受け取る(
waitする)までは、プロセス情報が一時的に残り続けます。この状態をゾンビプロセスと呼びます。ps auxのSTAT欄に Z と表示されているプロセスがこれにあたります。逆に、子プロセスより先に親プロセスが終了してしまった場合、その子プロセスは孤児プロセスと呼ばれ、多くの場合PID 1のプロセスに引き取られます。どちらもLinuxのプロセス管理の仕組みを理解するうえで押さえておきたい用語です。
ls -l /home を実行する際の流れ。入力後、エイリアスやワイルドカードなどを展開し、PATHから実行ファイルを検索したうえで、fork()で子プロセスを作り、exec()でそのプログラムに置き換えて実行します。終了するとプロンプトに戻ります。なぜこの順番になるのか — シェルは「通訳者」
先ほどの図の5つのステップを、もう少しゆっくり見ていきましょう。ここで押さえておきたいのは、私たちがキーボードで打っているlsやrmという文字を、Linux本体(カーネル)がそのまま理解しているわけではない、ということです。間に立って、私たちの入力を「実行できる形」に変換しているのがシェルです。レストランで、お客さんの注文をそのまま厨房に伝えるのではなく、店員が在庫確認や伝票の記入といった下準備をしてから厨房に回すのに似ています。この「下準備」にあたるのが、次に説明する②〜④のステップです。
① プロンプト表示と入力受付
ターミナルに$(管理者の場合は#)が表示されている状態をプロンプトと呼びます。「今、次のコマンドを受け付けられますよ」というシェルからの合図です。ここで文字を打っている間、最後にEnterキーを押すまでは、シェルはまだ何も実行しません。単に「入力中の文字列」として溜めているだけです。
② 展開 — 省略形を正式な形に書き換える
Enterキーが押されると、シェルはまず入力された行全体を読み取り、いくつかの「省略記法」を正式な文字列に書き換えます。この作業を展開(てんかい/expansion)と呼びます。初級の範囲でまず知っておきたいのは、第3章で登場した*のようなワイルドカード(ファイル名を一括で指定するための記号)の展開です。これについては、この章の後半で詳しく説明します。
展開には他にもいくつか種類があります。参考までに、簡単な例を挙げておきます(詳しい使い方は中級以降で扱います)。
| 展開の種類 | 簡単な例 |
|---|---|
| ワイルドカード展開 | *.txt → 実在する.txtファイルの名前一覧 |
| チルダ展開 | ~ → 自分のホームディレクトリの絶対パス |
| 変数展開 | $HOME → 変数HOMEに入っている値 |
大事なのは、これらの展開がすべてコマンドの実行よりも前に、シェルの手元で終わっているという点です。この後の③・④のステップでは、すでに展開が終わった「正式な文字列」だけが使われます。
③ 実行ファイルの検索(PATH)
展開が終わると、シェルは打たれたコマンド名(例:ls)に対応するプログラム本体のファイルを探しに行きます。探す場所は無限にあるわけではなく、PATHという環境変数に登録された、決まったディレクトリの一覧の中だけを順番に探します。おなじみの「command not found(コマンドが見つかりません)」というエラーは、実はこの検索に失敗したときに表示されるものです。
④ fork と exec — プロセスの複製と入れ替え
目当てのプログラムが見つかると、シェルは自分自身のコピーを1つ作ります。この「自分を複製する」操作をfork(フォーク)と呼びます。複製されたコピー(子プロセス)は、続いてexec(エグゼク)という操作によって、自分の中身をまるごと目的のプログラム(例:ls本体)に置き換えます。シェルが自分の「分身」を1体作り、その分身に変身の魔法をかけて別のプログラムに姿を変えさせている、とイメージすると分かりやすいかもしれません。プロセスという入れ物(PID)は変わらず引き継がれますが、中で動いているプログラムの中身はまったくの別物になります。
⑤ 終了と終了ステータス
プログラムの実行が終わると、そのプロセスは終了ステータス(exit status)という番号をシェルに返して消えます。慣習として、0は「正常終了」、それ以外の数字は「何らかの異常があった」ことを表します。シェルはこの番号を受け取ると、プロンプトを再び表示し、次の入力を待つ状態に戻ります。実機では直前のコマンドの終了ステータスをecho $?で確認できます。
ls -l /home > result.txt のように>を使うリダイレクト(出力先の切り替え)の設定は、②の展開が終わったあと、③・④に進む前の段階でシェルによって準備されます。コマンド本体(この例ではls)は、自分の出力が画面に表示されているのかファイルに書き込まれているのかを、実は気にしていません。リダイレクトの詳しい仕組みは「パイプとリダイレクト」の章で扱います。
ワイルドカード(*)を展開しているのは「コマンド」ではなく「シェル」
第3章で登場した*などのワイルドカードについて、もう一歩踏み込んで説明します。多くの人は「rm *.logを実行すると、rmというコマンドが賢く『.logで終わるファイルを探して消す』処理をしてくれている」と誤解しがちです。しかし実際には、rm自身はワイルドカードの意味をまったく理解していません。*.logという文字列を、実在するファイル名の一覧に置き換えているのは、②のステップで説明したシェルです。
つまり、カレントディレクトリにaccess.logとerror.logという2つのファイルがある状態でrm *.logと打つと、rmが受け取る直前には、シェルによってすでに次のように書き換えられています。
$ rm *.log
# ↓ シェルが展開したあと、rmに実際に渡される内容はこうなっている
$ rm access.log error.log
rmから見れば、最初からrm access.log error.logと打たれたのか、rm *.logと打たれて後から展開されたのかを区別する方法はありません。この仕組みを理解しておくと、次の2つの「なぜ」が腑に落ちるはずです。
「
*」だけを単独で書くと「カレントディレクトリにある、隠しファイルを除くすべてのファイル・ディレクトリ名」に展開されます。つまりrm *は、シェルによって「今ここに見えている全部を消して」という命令に化けてからrmに渡されているのです。rm自体には「本当に全部消していいですか?」と確認する機能は基本的にありません(第4章で触れたrm -iを除く)。実行する前に、まずls *やls *.logで「シェルが何に展開するか」を目で確認してからrmに切り替える習慣が、事故を防ぐいちばんの近道です。
第3章の例で
find . -name "*.txt"と、*.txtを引用符で囲んでいたことに気づいたでしょうか。これは、シェルに*.txtを勝手に展開させたくないからです。findコマンドは、パターンにあたる文字列(*.txtという「形」そのもの)を自分自身で処理したいので、シェルに展開されて具体的なファイル名1つに化けてしまっては困ります。引用符("や')で囲むと、シェルはその中身を「展開せず、そのままの文字列として渡してほしい」という意味だと解釈します。「引用符をつけるかどうか」は、そのコマンドに「パターンの形」を渡したいのか、「展開済みの具体的な名前」を渡したいのかで決まる、と覚えておくとよいでしょう。
①
find . -name "*.txt"(引用符あり)②
find . -name *.txt(引用符なし。カレントディレクトリに.txtファイルが1つでもあると、意図と違う挙動になることがあります)同じように見えるコマンドでも、引用符の有無で結果が変わることがある、という感覚をつかんでおきましょう。
top — リアルタイムで監視する
top を実行すると、CPUやメモリ使用率が高い順にプロセスがリアルタイムで表示され続けます。終了するには q キーを押します。サーバーが重いときにまず確認するコマンドの一つです。近年ではより見やすい表示や操作性を持つ htop という改良版が使われることも増えています。
topの画面では、プロセス一覧に加えて画面上部に全体のCPU使用率やメモリの空き容量、システムの平均負荷(load average)といった情報もまとめて表示されます。個別のプロセスだけでなく、システム全体がどれくらい忙しいかを一目で把握できるのがtopの強みです。topの実行中に P を押すとCPU使用率順、M を押すとメモリ使用率順に並び替えられるなど、キー操作で表示を切り替えられる点も覚えておくと便利です。
load average は「1分・5分・15分」という3つの時間幅で平均した、実行待ちのプロセス数の目安です。たとえばCPUのコア数が4つのマシンで load average が継続的に4を超えているようであれば、処理待ちが発生している可能性が高いと判断できます。詳しいトラブルシューティングの流れは、トラブルシューティングの「サーバーが重い・応答が遅い」の記事でも扱っています。
プロセスを名前で探す — pgrep
PIDを調べるために毎回 ps aux を全部読むのは大変です。実機では pgrep プロセス名 というコマンドで、名前が一致するプロセスのPIDだけを素早く絞り込めます。似た働きをするコマンドに、名前が一致するプロセスへ直接シグナルを送れる pkill プロセス名 もあります。ps aux | grep nginx のようにpsとgrepを組み合わせる方法も広く使われていますが、pgrep/pkillの方が意図がはっきりして誤操作しにくいという利点があります。
バックグラウンド実行と jobs / fg / bg
コマンドの末尾に & を付けると、処理をバックグラウンドで実行しつつ、すぐにターミナルの操作を続けられます。すでにフォアグラウンドで動かしてしまった処理を後からバックグラウンドに回したい場合は、Ctrl+Z でいったん一時停止してから bg で再開する、という流れもよく使われます。
| コマンド | 意味 |
|---|---|
jobs | 現在のシェルが持つバックグラウンドジョブの一覧を表示 |
fg %番号 | 指定したジョブをフォアグラウンドに戻す |
bg %番号 | 一時停止中のジョブをバックグラウンドで再開する |
| Ctrl+Z | フォアグラウンドで動いているプロセスを一時停止(サスペンド)する |
$ long-task.sh &
[1] 1523
$ jobs # バックグラウンドの一覧
[1]+ Running long-task.sh &
$ fg %1 # フォアグラウンドに戻す
long-task.sh
(フォアグラウンドで実行が継続する。処理が終わるか、Ctrl+Cで中断するかCtrl+Zで再び一時停止するまでプロンプトには戻らない)
この行のこの値に注目: fgを実行すると、まず「今からどのコマンドをフォアグラウンドに戻すか」を示すため、そのコマンドライン自体(long-task.sh)が画面にエコー表示されます。その後は通常どおりフォアグラウンドで実行されるため、処理が終わるかCtrl+C/Ctrl+Zを押すまでシェルのプロンプトには戻りません。
ジョブ番号(%1など)とPIDは別物です。ジョブ番号はあくまで「今のシェルの中だけ」で通用する通し番号で、killにはPIDを渡すのが基本ですが、実機ではkill %1のようにジョブ番号を直接指定することもできます。
ここで1つ注意点があります。&で開始したバックグラウンド処理は、そのシェル(ターミナル)自体を閉じたりSSH接続を切断したりすると、一緒に終了してしまうのが基本の挙動です。ログアウト後もサーバー上で処理を継続させたい場合は、実機ではnohup コマンド &のようにnohupを付けて起動するか、時間のかかる処理は次章以降で扱うシェルスクリプトとしてsystemdのサービスに登録するなど、目的に応じた方法を使い分けます。
見た目が似ているこの3つのキー操作は、それぞれまったく異なる働きをします。混同して使うと「終了させたかったのに一時停止しただけだった」という戸惑いにつながるので、ここで整理しておきましょう。
| キー操作 | 働き |
|---|---|
| Ctrl+C | 実行中のプロセスにSIGINT(割り込み信号)を送り、処理そのものを終了させる。 |
| Ctrl+Z | 実行中のプロセスをSIGSTOPで一時停止(サスペンド)させる。プロセスは終了せず、bgやfgで再開できる状態のまま裏に残る。 |
| Ctrl+D | プロセスを終了させる信号ではなく、標準入力の「入力終了(EOF)」をシェルや実行中のプログラムに伝える。何も入力せずにシェルで押すと、そのシェル自体からログアウトする。 |
kill — プロセスを終了させる
kill はプロセスに「シグナル」という終了指示を送るコマンドです。よく使うシグナルは次の2つです。
| シグナル | 意味 |
|---|---|
SIGHUP(-1) | 端末との接続が切れたことを通知する。設定ファイルの再読み込みに使うプログラムも多い。 |
SIGINT(-2) | Ctrl+Cを押したときに送られる割り込み信号。 |
SIGTERM(既定・-15) | 「終了してください」という穏やかな終了要求。後片付けの時間が与えられる。 |
SIGKILL(-9) | 問答無用で即座にプロセスを強制終了させる。 |
SIGSTOP(-19) | プロセスを強制的に一時停止させる。プロセス側で無視することはできない。 |
SIGCONT(-18) | SIGSTOPなどで一時停止したプロセスの実行を再開させる。 |
$ kill 1523 # SIGTERMを送る(穏やかな終了)
$ kill -9 1523 # SIGKILLを送る(強制終了)
$ kill -l # 利用できるシグナルの一覧を表示(実機)
プロセスはSIGTERMを受け取ると、あらかじめプログラム側で用意しておいた「終了時の後片付け処理」(開いているファイルを閉じる、一時ファイルを消す、通信中の相手に切断を伝えるなど)を行ってから終了できます。一方でSIGKILLはOSレベルで即座にプロセスを止めてしまうため、プログラム側は何の処理もできません。「まず優しく頼み、それでもダメなら強制する」という段階的な対応が、実務での基本的な進め方です。
プロセスの優先度 — nice / renice
複数のプロセスが同時にCPUを使おうとする場合、Linuxはnice値と呼ばれる優先度の指標をもとにCPU時間の配分を調整します。nice値は-20(最優先)〜19(最低優先)の範囲で設定でき、値が小さいほど優先度が高くなります。実機では、プロセスを起動する時点で優先度を指定するniceコマンドや、すでに動いているプロセスの優先度を後から変更するreniceコマンドが使われます。バックグラウンドで重い集計処理を回しつつ、他の作業を優先したいときなどに使う機能です。
kill -9 はプロセスに後片付けの猶予を与えないため、データが壊れる可能性があります。まずは通常の kill(SIGTERM)を試し、それでも終了しない場合の最終手段として使いましょう。
kill -HUP PID(SIGHUP)を送る、という運用がよく行われます。プロセスによってシグナルの受け取り方は異なるため、対象のソフトウェアのドキュメントで対応状況を確認するのが確実です。
「サーバーが重い」と感じたときの最初の一手
本章で扱ったps・top・killは、サーバーの動作がおかしいと感じたときに最初に頼るツールです。まずtopで全体のCPU・メモリ使用率を確認し、突出して高い数値を出しているプロセスがあればそのPIDを特定します。原因のプロセスが見つかったら、いきなりkill -9で強制終了するのではなく、まずそのプロセスが何のソフトウェアなのかをps -p PID -o cmdなどで確認し、本当に停止して問題ないかを判断してから対処するのが安全な進め方です。原因不明のまま闇雲にプロセスを止めると、別のサービスに予期しない影響が及ぶことがあります。
手を動かす小課題
ps・top・killは実機専用のコマンドのため、練習用ターミナルでは代わりに、この章で学んだ「シェルの展開」の仕組みを実際に確認してみましょう。答えは見出しをクリックすると表示されます。
課題1: ls *.txtとlsの結果を見比べて、ワイルドカードが展開されている様子を確認してみましょう。
答え: lsだけだとすべてのファイルが表示されるのに対し、ls *.txtは.txtで終わるファイルだけに絞り込まれます。この絞り込みはls自身が行っているのではなく、シェルが実行前に*.txtを実在するファイル名に置き換えているのでした。
課題2: find . -name "*.txt"(引用符あり)を実行してみましょう。
答え: find . -name "*.txt"と入力すると、カレントディレクトリ以下から.txtファイルが検索されます。引用符で囲むことで、シェルに展開させず「パターンの形」のままfindに渡している、という点を思い出しながら実行してみましょう。
課題3: 存在しないコマンド(例: foobar)を打って、command not foundが表示される様子を確認してみましょう。
答え: foobarのように存在しないコマンド名を入力すると、シェルがPATHの中を探しても見つからず、command not foundが表示されます。この章で説明した「③実行ファイルの検索」の段階で失敗している、というのが実際に起きていることです。