
Linuxはなぜプロセスをファイルとして見せるのか
端末でpsを実行すると、動いているプロセスのPID、親子関係、状態、実行時間が一覧になる。
$ ps -o pid,ppid,state,comm
PID PPID S COMMAND
3841 3712 S bash
4027 3841 R pspsがカーネル内部のデータ構造を直接読むわけではない。Linux版のpsは、主に/proc以下を読み、表示に必要な情報を組み立てている。procps
同じPIDを自分で読んでみると、その材料が見える。
$ sed -n '1,8p' /proc/$$/status # $$は現在のシェルのPID
Name: bash
Umask: 0002
State: S (sleeping)
Tgid: 3841
Ngid: 0
Pid: 3841
PPid: 3712
TracerPid: 0statusは普通のテキストファイルに見える。しかし、SSDやHDDにこの内容が保存されているわけではない。プロセスの状態が変われば表示も変わり、プロセスが終われば/proc/3841というディレクトリごと消える。
Linuxは、実行中のプロセス情報をファイルとして公開している。
この仕組みがprocfsである。
この記事では、/proc/<PID>と/proc/selfを使い、Linuxがプロセス情報をどのように公開しているかを説明する。
status、cmdline、environ、cwd、exe、fdを順に確認する。各項目の意味だけでなく、ファイルとして公開する理由、情報の信頼性、スレッドやnamespaceによる見え方の違いも扱う。
プロセスを一つのファイルにした
procfsの発想はLinuxより古い。
1984年6月、AT&T Bell LaboratoriesのT. J. Killianは、USENIXの夏季会議でProcesses as Filesという論文を発表した。対象は研究版のEighth Edition UNIXである。killian
当時、デバッガーが別のプロセスを調べる標準的な手段にはptrace()があった。しかしKillianは、親子関係への依存、調査対象を停止させる手間、一語ずつデータを転送する低い帯域などを問題として挙げた。
そこで、プロセスのアドレス空間をファイルとして開く仕組みを作った。
/proc/00001
/proc/00002
/proc/02596当時の/procは、現在のようにPIDごとのディレクトリがあり、その下へstatusやfdが並ぶ形ではない。PIDごとに一つのファイルがあり、そのファイルのオフセットがプロセスの仮想アドレスに対応していた。
デバッガーは通常のファイルと同じようにopen()し、lseek()でアドレスを選び、read()やwrite()でプロセスのメモリを調べられる。所有者や読み書きの許可には、ファイルシステムの権限モデルを流用できた。
flowchart LR
D[デバッガー] -->|open| P["/proc/02596"]
D -->|lseek| A[調べる仮想アドレス]
D -->|read / write| M[対象プロセスのメモリ]
P --> A --> M論文の最後では、この方式で書いたpsが従来版よりおよそ4倍速かったと報告されている。正確な倍率は当時のVAXと実装に依存するが、プロセス情報を得るためにカーネル内部の「汚いデータ構造」を直接探らずに済む利点は明快だった。
1991年、Roger FaulknerとRon Gomesは、UNIX System V Release 4のprocfsを報告した。procfsはデバッガー用のptrace()代替から、プロセス状態と制御を扱う一般的なインターフェースへ広がっていた。さらに、複数スレッドを表しやすくするため、PID一個につき一ファイルという平坦な構造を、状態ファイルや制御ファイルを持つディレクトリ階層へ変える案も示された。svr4-proc
現在のLinuxで見慣れた/proc/<PID>/statusや/proc/<PID>/task/は、この「プロセスをファイルシステムの階層として表す」という方向にある。
Linuxへ来たprocfs
Linuxにprocfsが加わったのは、1992年9月5日に公開された0.97 patchlevel 3だった。
当時のリリース告知には、変更点の一つとして「ユーザーのメモリやファイルなどへアクセスするためのproc-fs」が挙げられている。続く説明では、次のようなマウント方法が示されていた。linux-0973
# mount -t proc /dev/ram /proc/dev/ramの内容を使うわけではない。当時のマウント用インターフェースでは何らかのデバイスを指定する必要があり、procfsには不要なデバイス名をダミーとして渡していた。
Linus Torvaldsは同じ告知で、procfsのコードは「あまり美しくない」とし、必要なときに読み込める形へ整理できなければ、次の0.98から外すかもしれないとも書いている。
実際には外れなかった。
ほどなく、外部デバイスを持たないprocfsのような内部ファイルシステムを扱いやすくする変更も加わった。現在は、次のようにファイルシステム種別を指定してマウントできる。linux-0983
# mount -t proc proc /procLinuxのprocfsは、その後、メモリ、CPU、割り込み、ネットワーク、マウント状態、カーネル設定へ公開対象を広げた。
名称はprocess filesystemのままだが、Linuxではプロセス以外のカーネル情報も数多く公開するようになった。
/procはディスクにないファイルシステム
procfsは、ストレージ上のデータではなく、カーネルが持つ現在の状態をファイルとして見せる。手元で何としてマウントされているかを確認しよう。
$ findmnt -no TARGET,FSTYPE,OPTIONS /proc
/proc proc rw,nosuid,nodev,noexec,relatimeファイルシステム種別はprocである。ext4やBtrfsのように、ブロックデバイスへ保存されたデータを管理するファイルシステムではない。
procfsは、カーネル内部の情報をVFSへ接続する擬似ファイルシステムである。VFSは、異なるファイルシステムへopen()、read()、stat()などの共通操作を提供するLinuxカーネルの層を指す。vfs
通常のファイルとの違いを、/proc/uptimeで確認する。
$ stat -c 'size=%s bytes' /proc/uptime
size=0 bytes
$ cat /proc/uptime
349821.47 2707312.09statで見たサイズは0なのに、読むと内容が返ってくる。
通常のファイルでは、ストレージに保存されたバイト列を読む。/proc/uptimeでは、読み取り要求を受けたprocfsの処理が、その時点でカーネルの持つ稼働時間を文字列へ変換する。
P1: box "cat /proc/uptime" width 1.55in height 0.55in
P2: box "VFS / procfs" width 1.35in height 0.55in with .w at 0.35in right of P1.e
P3: box "カーネルが持つ" "現在の状態" width 1.55in height 0.7in with .w at 0.35in right of P2.e
P4: box "読み取り時に" "内容を生成" width 1.55in height 0.7in with .w at 0.35in right of P3.e
arrow from P1.e to P2.w
arrow from P2.e to P3.w
arrow from P3.e to P4.w
F1: box "cat notes.txt" width 1.55in height 0.55in at 1.15in below P1
F2: box "VFS / 通常のFS" width 1.35in height 0.55in with .w at 0.35in right of F1.e
F3: cylinder "SSD・HDD上の" "保存済みデータ" width 1.55in height 0.7in with .w at 0.35in right of F2.e
arrow from F1.e to F2.w
arrow from F2.e to F3.w「ファイルである」は「ディスクへ保存されている」と同義ではない。
ファイルという共通の操作方法を通して、保存データだけでなく、端末、デバイス、カーネル状態へもアクセスできる。procfsはUnixのこの性質を、プロセスへ当てはめたものだ。
ただし、ファイルに見えるからといって、通常のファイルと同じ性質をすべて持つわけではない。サイズが意味を持たないこともあれば、seekできないもの、読み取り中に状態が変わるもの、書き込むとカーネル設定が変わるものもある。
したがって、procfsの各ファイルを通常のファイルと同じ前提で扱うことはできない。読み取り方法、更新のタイミング、書き込みの効果は項目ごとに確認する必要がある。
プログラムとプロセスは別のもの
/proc/<PID>を読む前に、プログラムとプロセスを分けておく。
/usr/bin/bashは、ストレージ上にある実行ファイルである。まだ動いていない状態でも存在する。
そのファイルを実行すると、カーネルは仮想アドレス空間、CPUの実行状態、認証情報、開いているファイル、カレントディレクトリなどを持つ実行主体を管理する。これがプロセスである。
同じプログラムから複数のプロセスを起動できる。
$ sleep 600 &
[1] 4312
$ sleep 600 &
[2] 4313
$ ps -o pid,ppid,comm -p 4312,4313
PID PPID COMMAND
4312 3841 sleep
4313 3841 sleep実行ファイルはどちらも/usr/bin/sleepだが、PIDは別であり、待機時間や開いているファイルなどの状態も別々に持てる。
flowchart TB
E["実行ファイル<br>/usr/bin/sleep"]
E --> P1["プロセス<br>PID 4312"]
E --> P2["プロセス<br>PID 4313"]「プロセスは実行中のプログラム」と説明されることが多いが、より正確には、プログラムのコードを含む実行中の状態一式である。
/proc/<PID>は、その状態一式を項目ごとに見せる。
/proc/<PID>は生きているプロセスの目録
/proc/<PID>には、そのPIDを持つプロセスの状態が項目ごとに並ぶ。sleep 600を一つ起動して、中を見てみよう。
$ sleep 600 &
[1] 4312
$ pid=$! # 直前にバックグラウンド起動したプロセスのPIDを保存
$ ls /proc/$pid | sed -n '1,20p'
arch_status
attr
autogroup
auxv
cgroup
clear_refs
cmdline
comm
coredump_filter
cpu_resctrl_groups
cwd
environ
exe
fd
fdinfo
gid_map
io
limits
loginuid
map_files環境やカーネル設定によって項目は増減するが、役割は大きく分けられる。
| 種類 | 代表的なパス | 見えるもの |
|---|---|---|
| 身元と状態 | status、stat、comm |
PID、親PID、状態、名前、認証情報 |
| 起動時の情報 | cmdline、environ、auxv |
引数、初期環境、カーネルから渡された補助情報 |
| ファイルとの関係 | cwd、root、exe、fd/ |
作業場所、ルート、実行ファイル、開いている対象 |
| メモリ | maps、smaps、statm |
仮想アドレス空間とメモリ使用量 |
| 隔離と管理 | ns/、cgroup、uid_map |
namespace、cgroup、IDの対応 |
| スレッド | task/ |
同じプロセスに属する各スレッド |
この一覧をすべて覚える必要はない。まずstatus、cmdline、exe、fdを押さえれば、障害調査で「何が動き、どこから起動され、何を開いているか」をかなり追える。
/proc/<PID>はログではなく、現在のプロセスに結び付いた表示である。
$ sleep 600 &
[1] 4312
$ pid=$!
$ test -d /proc/$pid && echo exists # ディレクトリが存在するときだけ表示
exists
$ kill $pid # PIDを指定したプロセスへ終了シグナルを送る
$ wait $pid 2>/dev/null # 終了を待ち、エラーは表示しない
$ test -d /proc/$pid || echo gone # ディレクトリがないときだけ表示
goneプロセスが終了すると、そのPIDに対応するディレクトリも消える。
statusは人が読める要約
statusは、PID、状態、認証情報、スレッド数などを、人が追いやすい形へまとめる。もう一度sleepを起動して読んでみよう。
$ sleep 600 &
[1] 4470
$ pid=$!
$ grep -E '^(Name|State|Tgid|Pid|PPid|Uid|Gid|Threads):' /proc/$pid/status # statusから指定した項目だけを抽出
Name: sleep
State: S (sleeping)
Tgid: 4470
Pid: 4470
PPid: 3841
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
Threads: 1statusは、プロセス情報を人が読みやすい名前: 値の形式でまとめたファイルである。psの表示と重なる項目も多い。
StateのSは割り込み可能な待機状態を示す。sleepはCPUを使い続けて時間を数えているのではなく、指定時間が過ぎるまでカーネルに待たされているため、この状態になる。
よく見る状態は次の通り。
| 文字 | おおまかな意味 |
|---|---|
R |
実行中、または実行可能 |
S |
割り込み可能な待機 |
D |
割り込み不能な待機 |
T |
シグナルなどによる停止 |
Z |
終了したが、親に回収されていないzombie |
UidとGidには四つの値が並ぶ。実UID、実効UID、保存set-user-ID、ファイルシステムUIDなどであり、権限判定やsetuidプログラムを理解するときに重要になる。ここでは「一つのプロセスにも、用途の異なる複数のIDがある」と分かればよい。proc-status
statusは読みやすいが、機械処理用の安定した表形式ではない。カーネルの更新で項目が追加されることがあるため、ツールを書くなら未知の行を無視し、必要な名前だけを拾う方が安全である。
/proc/selfの「自分」は誰か
/proc/selfは、アクセスしたプロセス自身を指す特別なパスである。
問題は、その「自分」が誰なのかだ。
$ echo $$
3841
$ readlink /proc/self # readlink自身から見た/proc/selfのPIDを表示
4598$$は現在のシェルのPIDなので、最初の値はbashのPIDである。
一方、二行目の/proc/selfを読んだのは、シェルから起動されたreadlinkプロセスである。そのため、表示されたのはreadlink自身のPIDになる。
sequenceDiagram
participant Shell as bash (PID 3841)
participant Readlink as readlink (PID 4598)
participant Proc as procfs
Shell->>Readlink: readlink /proc/self を実行
Readlink->>Proc: /proc/self を参照
Proc-->>Readlink: /proc/4598 として解決別の例も見てみよう。
$ cat /proc/self/comm
cat/proc/self/commを開いたのはcatなので、catという名前が返る。
一つのプログラムの中でPIDと/proc/selfを比べれば、同じ値になる。
$ python3 -c 'import os; print(os.getpid()); print(os.readlink("/proc/self"))' # Python自身のPIDと/proc/selfの解決先を表示
4621
4621/proc/selfは、特定のPIDへ固定された普通のシンボリックリンクではない。アクセスしているタスクに応じて行き先を決める、procfsの特別なリンクである。proc-self
シェル自身を見たいなら、シェルが展開する$$を使う。
$ readlink /proc/$$/exe
/usr/bin/bash
$ grep '^State:' /proc/$$/status
State: S (sleeping)プログラム自身が自分を調べるコードでは、PIDを文字列へ変換せずに済む/proc/selfが便利である。
comm、cmdline、exeは同じ名前ではない
プロセスの「名前」に関係する情報は、procfsの複数のファイルに分かれている。
| パス | 主に表すもの |
|---|---|
comm |
カーネルが持つ短いタスク名 |
cmdline |
execve()時の引数が置かれたメモリ領域 |
exe |
実行中のプログラムに対応する実行ファイル |
sleep 600で比べる。
$ sleep 600 &
[1] 4470
$ pid=$!
$ cat /proc/$pid/comm
sleep
$ tr '\0' '\n' < /proc/$pid/cmdline # NUL区切りの引数を改行区切りで表示
sleep
600
$ readlink /proc/$pid/exe
/usr/bin/sleep三つは似ているが、同じ情報の別表記ではない。
commはスレッドごとに設定できる短い名前である。cmdlineには引数がNUL文字で区切られて並ぶ。exeは実行ファイルへ結び付いたリンクである。
cmdlineを単なる「起動コマンドの永久保存」と考えるのは危険だ。
実行中のプロセスは、引数文字列が置かれたメモリを書き換えられる。さらに特権があれば、カーネルへcmdlineとして見せるメモリ範囲そのものを変更できる。zombieでは空になる。Linuxのマニュアルは、これは「プロセスが見せたいコマンドライン」だと考えるよう注意している。cmdline
プロセス一覧の表示名を偽装できるということでもある。したがって、cmdlineだけを実行ファイルの本人確認に使うべきではない。
environに見えるのは初期環境
environには、現在実行中のプログラムがexecve()で開始されたときの環境が、NUL区切りで入っている。
$ DEMO_VALUE=hello sleep 600 & # このsleepだけに環境変数を設定
[1] 4772
$ pid=$!
$ tr '\0' '\n' < /proc/$pid/environ | grep '^DEMO_VALUE=' # DEMO_VALUEだけを抽出
DEMO_VALUE=helloPATH、HOME、言語設定など、プログラムがどの環境で起動されたかを調べるのに役立つ。
起動後に変更された環境変数については注意が必要である。
プログラムが起動後にsetenv()やputenv()で環境を変更しても、/proc/<PID>/environがその変更を反映するとは限らない。ここで見えるのは、原則としてexecve()時に渡された初期環境である。参照するメモリ範囲を変更できる仕組みもある。environ
環境変数には、アクセストークン、接続文字列、プロキシーの認証情報などが入る場合がある。便利だからと何でも環境変数へ置くと、procfs、core dump、ログ、子プロセスへの継承といった経路から見える範囲も増える。
環境変数へ秘密情報を入れる場合は、procfsだけでなく、core dump、ログ、子プロセスへの継承などから参照される可能性も考慮する必要がある。procfsの読み取り制限だけに依存して秘密を保護するべきではない。
cwd、root、exeは普通のシンボリックリンクではない
cwd、root、exeは、ls -lではシンボリックリンクに見える。
$ sleep 600 &
[1] 4772
$ pid=$!
$ readlink /proc/$pid/cwd
/home/demo
$ readlink /proc/$pid/root
/
$ readlink /proc/$pid/exe
/usr/bin/sleep| パス | 意味 |
|---|---|
cwd |
プロセスのカレントディレクトリ |
root |
そのプロセスから見たルートディレクトリ |
exe |
実行中のプログラムに対応する実行ファイル |
これらは、単に保存されたパス文字列を返す通常のsymlinkではない。カーネルが保持するディレクトリやファイルの参照へ結び付くmagic linkである。
通常のsymlinkは、リンク内に保存されたパスをもう一度辿る。procfsのmagic linkは、対象となるカーネル内のファイル参照へ直接移動できる。readlinkが返す文字列は、その対象を説明するための名前であり、必ずしも同じパスから再び到達できるとは限らない。magic-link
この違いは、ファイルを削除したときに見えやすい。
fdはプロセスが開いているものを見せる
/proc/<PID>/fd/には、プロセスが持つファイルディスクリプターが並ぶ。
$ ls -l /proc/$$/fd/{0,1,2}
lrwx------ 1 demo demo 64 Aug 6 10:20 /proc/3841/fd/0 -> /dev/pts/2
lrwx------ 1 demo demo 64 Aug 6 10:20 /proc/3841/fd/1 -> /dev/pts/2
lrwx------ 1 demo demo 64 Aug 6 10:20 /proc/3841/fd/2 -> /dev/pts/2| 番号 | 慣用名 | 役割 |
|---|---|---|
0 |
標準入力 | キーボード、ファイル、pipeなどからの入力 |
1 |
標準出力 | 通常の出力先 |
2 |
標準エラー | エラーの出力先 |
通常のファイルだけでなく、pipe、socket、eventfd、epollなどもファイルディスクリプターとして現れる。
pipe:[183920]
socket:[184011]
anon_inode:[eventpoll]ファイルディスクリプターは、プロセス内の小さな整数である。その整数は、カーネルが管理する「開かれたファイル」の状態へつながる。現在の読み書き位置やopen時のフラグは、fdinfoから確認できる。
$ cat /proc/$$/fdinfo/0
pos: 0
flags: 0100002
mnt_id: 27
ino: 5削除しても、開いているファイルは残る
一時ファイルを開いたまま、ディレクトリから名前を消してみる。
$ tmp=$(mktemp)
$ exec 3<>"$tmp" # FD 3を作り、一時ファイルを読み書き用で開く
$ printf 'before unlink\n' >&3 # FD 3へ書き込む
$ rm "$tmp"
$ readlink /proc/$$/fd/3 # FD 3が参照している対象を確認
/tmp/tmp.Bx5vPbQ0mE (deleted)
$ printf 'after unlink\n' >&3 # ファイル名を削除した後もFD 3へ書き込む
$ cat /proc/$$/fd/3
before unlink
after unlink
$ exec 3>&- # FD 3を閉じるrmが消したのはディレクトリにある名前である。シェルが持つファイルディスクリプター3は、開かれたファイルを参照し続けている。そのため、さらに書き込める。ここではFD 3を読み書き両用で開いたので、/proc/$$/fd/3から開き直して内容も読める。
fd以下のmagic linkが単なる古いパス文字列ではなく、開かれた対象へ結び付いていることが分かる。
この性質は障害調査でも役立つ。ログファイルを削除したのにディスク容量が戻らない場合、どこかのプロセスがそのファイルを開いたままかもしれない。lsof +L1は、こうしたリンク数0のopen fileを探す代表的な方法である。
/proc/self/fd/0をファイル名として渡す
ファイル名しか受け取らないプログラムへ標準入力を渡したい場合、/proc/self/fd/0を使えることがある。
$ printf 'hello\n' | cat /proc/self/fd/0 # pipeの入力をcat自身の標準入力として読む
hellocatから見た/proc/self/fd/0は、cat自身の標準入力である。
Linuxの/dev/fdは、環境によって/proc/self/fdへつながっている。これにより、ファイル名を受け取るプログラムへpipeの入出力を渡せる。proc-fd
ただし、リンクを開き直す際には対象のinodeやptrace相当の権限チェックが行われる。すでに持っているFDを使えることと、/proc/<PID>/fd/Nから新しいFDとして開き直せることは同じではない。
一つのPIDの下に複数のスレッドがいる
/proc/<PID>は主にプロセス単位の情報を示すが、task/を参照するとスレッド単位の状態も確認できる。現在のLinuxアプリケーションでは、複数のスレッドを持つことも珍しくない。
小さなPythonプログラムで二つのスレッドを動かす。
$ python3 -c 'import os,threading,time; threading.Thread(target=time.sleep,args=(60,)).start(); print(os.getpid(), flush=True); time.sleep(60)' & # スレッドを1本持つPythonプロセスを起動
[1] 5031
5031
$ pid=$!
$ ls /proc/$pid/task
5031 5032/proc/<PID>/task/には、そのプロセスに属する各スレッドがTIDごとのディレクトリとして並ぶ。
/proc/5031/task/5031
/proc/5031/task/5032Linuxカーネル内部では、プロセスとスレッドを共通のtaskとして扱う部分が多い。利用者から一つのプロセスに見えるスレッド群は、同じアドレス空間やファイルディスクリプター表などを共有しながら、スケジューリング状態やレジスターなどを個別に持つ。
statusにあるTgidはthread group ID、PidはそのtaskのIDである。通常の/proc/<PID>/statusでは、thread group leaderのため両者が同じになる。task/<TID>/statusを読めば、スレッドごとの差が見える。
アクセスしているプロセスを指す/proc/selfに対し、アクセスしているスレッドを指す/proc/thread-selfもある。後者はLinux 3.17で加わった。proc-task
$ python3 -c 'import os; print(os.readlink("/proc/self")); print(os.readlink("/proc/thread-self"))' # プロセス自身とスレッド自身のPIDを表示
5074
5074/task/5074単一スレッドのプロセスでは番号が同じになる。/proc/thread-selfは、ライブラリなどが呼び出し元スレッド自身の情報を参照するときに使える。
namespaceによって見える/procは変わる
ホストとコンテナでpsの結果が異なるのは、PID namespaceによって見えるPIDが分離されているためである。
LinuxのPID namespaceは、プロセスIDの見え方を分離する。同じプロセスが、ホスト側では大きなPID、コンテナ内ではPID 1として見えることもある。
procfsをマウントしたとき、そのprocfsはマウントを行ったプロセスのPID namespaceから見えるプロセスだけを/proc/<PID>として表示する。新しいPID namespaceを作ると、そのnamespaceに対応したprocfsを/procへマウントするのが一般的である。対応していないprocfsを使うと、psは意図したnamespaceとは異なるPID一覧を読むことになる。pid-namespace
現在のプロセスが属するnamespaceは、ns以下から確認できる。
$ readlink /proc/$$/ns/pid
pid:[4026531836]
$ readlink /proc/$$/ns/mnt
mnt:[4026531841]角括弧内の番号が同じプロセス同士は、同じnamespaceオブジェクトを参照している。
マウント状態も、プロセスが属するmount namespaceによって異なる。
$ readlink /proc/mounts # 現在のmount namespaceに対応するリンク先を確認
self/mounts/proc/mountsは、mount namespaceが導入された後、/proc/self/mountsへのリンクになった。つまり「システムに一つだけのマウント一覧」ではなく、読んでいるプロセスから見えるマウント一覧を返す。proc-mounts
/procの表示内容は、読み取るプロセスの権限とnamespaceに依存する。同じカーネル上でも、プロセスによって見えるPID、マウント、各種情報は異なりうる。
権限によって見えない情報もある
procfsには、コマンドライン、環境変数、ファイルディスクリプター、メモリ配置、認証情報まで現れる。そのため、すべての利用者がすべてのプロセスを自由に読めるわけではない。
ファイルごとに通常の所有権とmodeがあり、さらに多くの項目ではptraceに準じたアクセス判定が使われる。対象プロセスの資格情報、呼び出し側の権限、対象がcore dump可能な状態かどうか、Linux Security Moduleの判断などが関係する。
procfsのマウントにはhidepidオプションもある。
| 設定 | 他ユーザーの/proc/<PID> |
|---|---|
hidepid=0 |
従来どおり表示する |
hidepid=1 |
PIDディレクトリの存在は見せるが、中のファイルへのアクセスを制限する |
hidepid=2 |
他ユーザーのPIDディレクトリ自体を一覧から隠す |
現在の設定は次のように確認できる。
$ findmnt -no OPTIONS /proc
rw,nosuid,nodev,noexec,relatimehidepidがなければ通常は0相当であるが、ディストリビューション、組織のポリシー、コンテナ環境によって設定は異なる。proc-mount-options
観察している間にも対象は変わる
/proc/<PID>は現在の状態を示すが、複数ファイルの内容が同じ時点に固定されたスナップショットではない。
statusを読んだ直後にプロセスが別の状態へ移り、fdを読む前にファイルを閉じることがある。ls /proc/<PID>/fdで番号を見つけても、readlinkする前にそのFDが閉じられ、同じ番号が別のファイルへ再利用されるかもしれない。
PID自体も再利用される。
Linuxカーネル文書は、/proc/<PID>やその下のファイルをopenしていても、対象プロセスの終了後に同じPIDが別プロセスへ再利用されることを防げないと注意している。すでにopenしたprocfsのFDが偶然新しいプロセスへ作用することはなく、通常はESRCHなどで失敗するが、パスを番号から開き直すコードには競合が残る。proc-race
単発の調査なら、再確認すれば十分なことが多い。
監視ツールやプロセス管理ソフトウェアで対象を確実に追うなら、Linux 5.3以降のPID file descriptor、略してpidfdが役立つ。pidfd_open()はPIDで示したtaskを参照するFDを返し、その終了をpoll()で待ったり、pidfd_send_signal()で同じ対象へシグナルを送ったりできる。数字だけのPIDより、再利用に強いハンドルとして扱える。pidfd
procfsは、その時点の状態確認に向いている。長時間にわたって同じプロセスを追跡する用途では、PIDを含むパスだけに依存せず、pidfdなど再利用に強い仕組みを使う方が安全である。
ファイルとして見せる設計の利点と限界
procfsが便利なのは、専用のプログラムを書かなくても、既存の道具を使えるからである。
$ python3 -c 'import os,threading,time; threading.Thread(target=time.sleep,args=(60,)).start(); print(os.getpid(), flush=True); time.sleep(60)' &
[1] 5031
5031
$ pid=$!
$ grep '^Threads:' /proc/$pid/status
Threads: 2
$ readlink /proc/$pid/cwd
/home/demo
$ find /proc/$pid/fd -maxdepth 1 -type l -printf '%f -> %l\n' # FD番号とリンク先を一覧表示
0 -> /dev/pts/2
1 -> /dev/pts/2
2 -> /dev/pts/2cat、grep、readlink、シェルのリダイレクト、各プログラミング言語のファイルAPIをそのまま使える。1984年のprocfsが狙った「既存のファイル操作でプロセスへ触れる」という利点は、現在も残っている。
この方式には、次の制約がある。
- テキスト形式の項目は解析規則を必要とする
- ファイルごとに意味と権限判定が違う
- 複数ファイルをまたぐ一貫したスナップショットではない
- Linux固有の項目へ依存すると、ほかのUnixへ移植しにくい
- 便利なため、本来別の構造化APIが向く情報まで
/procへ増えやすい
ファイル操作へ統一しても、各項目の意味や整合性まで統一されるわけではない。
procfsは、異なる種類のカーネル情報を既存のファイル操作で扱えるようにした。Linuxではこの方式がプロセス情報以外にも広く使われるようになった。
procfs②ではmapsを読む
ここまでで、実行中のプロセスについて次のことが分かるようになった。
statusから、PID、親子関係、状態、認証情報を読む/proc/selfから、アクセスしたプロセス自身へ到達するcmdlineとenvironは、プロセスが持つメモリ上の情報を反映するcwd、root、exe、fdは、カーネル内のファイル参照へつながるtaskにはスレッドが並び、nsには所属するnamespaceが示される- procfsの情報は権限で制限され、時間とともに変化する
readlink /proc/<PID>/exeを実行すると、そのプロセスの主実行ファイルが一つ返る。
$ readlink /proc/$$/exe
/usr/bin/bashでは、プロセスのメモリには、この一つのファイルだけが置かれているのだろうか。
/proc/<PID>/mapsを開くと、同じbashのアドレス空間に、実行ファイル、共有ライブラリ、動的リンカ、stack、heap、カーネルがマップした領域まで並んでいる。
1984年のprocfsが最初にファイルとして公開したのも、プロセスのアドレス空間だった。procfs②では、現在のmapsを読み、実行ファイルがプロセスとして動き始めるまでを説明する。
脚注
- procps
procps-ngは、
ps、top、free、pmapなどを、通常/procにマウントされる擬似ファイルシステムから情報を得るツール群として説明している。procps-ng READMEを参照。Linuxカーネルのprocfs文書も、psがprocfsから情報を得ると説明している。 ← - killian
T. J. Killian, Processes as Files, USENIX Association Summer Conference, 1984, pp. 203–207。初期procfsはプロセスのアドレス空間をPID名のファイルとして公開し、通常のファイル権限、
lseek()、read()、write()を利用した。 ← - svr4-proc
Roger Faulkner and Ron Gomes, The Process File System and Process Model in UNIX System V, USENIX Winter 1991。SVR4のprocfsをプロセスモデル一般のインターフェースとして説明し、複数スレッドへ対応するため、PIDごとの単一ファイルからディレクトリ階層へ再構成する案を示している。 ←
- linux-0973
- linux-0983
- vfs
LinuxカーネルのOverview of the Linux Virtual File Systemを参照。VFSは、具体的なファイルシステム実装へ共通のファイル操作モデルを提供する。 ←
- proc-status
statusの各フィールドはLinuxカーネル文書The /proc Filesystemとproc_pid_status(5)を参照。状態文字はカーネルの版によって追加・整理されることがある。 ← - proc-self
/proc/selfの定義はproc_pid(5)を参照。PID namespace内でのreadlink()結果についてはpid_namespaces(7)に説明がある。 ← - cmdline
proc_pid_cmdline(5)は、引数文字列のNUL区切り、プロセス自身による内容や参照範囲の変更、zombieで空になることを説明している。 ← - environ
proc_pid_environ(5)を参照。environはexecve()時の初期環境を表し、起動後の通常の環境変更は反映しない。読み取りにはptrace由来のアクセス判定がある。 ← - magic-link
Linuxカーネル文書Pathname lookupは、
/proc/self/fd/1などを、文字列のパスではなく対象ファイルへの参照を返すmagic linkとして説明している。 ← - proc-fd
proc_pid_fd(5)は、通常ファイル、pipe、socket、anonymous inodeの表示、/proc/self/fd/Nを入出力ファイルとして使う方法、リンクを開き直す際の権限制約を説明している。 ← - proc-task
proc_pid_task(5)を参照。/proc/<PID>/task/<TID>はスレッドごとの情報を提供し、/proc/thread-selfはLinux 3.17以降で呼び出し元スレッド自身を指す。 ← - pid-namespace
pid_namespaces(7)は、PID namespaceごとの可視性とprocfsの関係を説明している。procfsは、そのマウントを行ったプロセスのPID namespaceから見えるプロセスを表示する。 ← - proc-mounts
proc_pid_mounts(5)を参照。/proc/mountsはLinux 2.4.19以降、読んでいるプロセスのmount namespaceを示す/proc/self/mountsへのリンクである。 ← - proc-mount-options
- proc-race
Linuxカーネル文書The /proc Filesystemは、open済みの
/proc/<PID>関連FDがPID再利用を防がないこと、対象終了後の操作は新しい同一PIDのプロセスへ作用せず通常失敗することを説明している。 ← - pidfd
pidfd_open(2)を参照。PID file descriptorはLinux 5.3で導入され、プロセス終了の監視、シグナル送信、waitid()、別プロセスのFD複製、namespace参照などに利用できる。 ←