えんでぃの技術ブログ

えんでぃの技術ブログ

ネットワークエンジニアの視点で、IT系のお役立ち情報を提供する技術ブログです。

ファイル実行時に呼ばれるfork,clone,execveについて調べてみた

20200927134745

お伝えしたいこと

Linuxでファイルを実行すると、子プロセスが生成します。
そして実行したプロセスが親プロセスになります。

その際、内部的にはforkとexecveというシステムコールが呼ばれていると聞いたことがあります。
私の環境 (Fedora35) で試してみたところ、forkではなくcloneが呼ばれ、その後execveが呼ばれていました。

本記事では、システムコール、fork、clone、execveという用語を整理したいと思います。

Linuxカーネル周りの技術に触れると、このあたりの用語が当たり前のように使われます。
そんなときに (私のように) 困らないよう、よろしければ読んでいってください。

今回の記事は情報ソースが少なめですが、実機でstraceコマンドを使った実験をすることで情報の信頼性を補完しています。

システムコールとは

システムコールとは、LinuxプロセスがLinuxカーネルに対して行う命令のことです1

Linuxでは、Linuxカーネルが全てのハードウェアに直接アクセスするなどの特権を持っています。

一般的なアプリケーションはこのような特権を直接行使することはできません。
代わりに、アプリケーションからカーネルに対してシステムコールを発行してカーネルに依頼を出します。
そしてカーネルが必要な処理を実行し、アプリケーションに処理結果 (戻り値) を返すという仕組みになっています。

ファイル実行時に呼ばれるシステムコール

bashシェル上でpsコマンド (/usr/bin/ps) を実行すると、bashを親プロセスとしてpsプロセスが生成します。
そして、psプロセスはpsコマンドの処理を実行します。

このようなファイル実行処理の裏では、以下の流れで処理が呼ばれています2

  1. 親プロセスがfork (またはclone) され、新たなPIDを持つ子プロセスが生成する
  2. 子プロセス上でexecveが実行され、親プロセスとは異なる動作を開始する

file_execution_flow

fork、clone、execveは、システムコールの一種です。
次のセクションでそれぞれの意味を紹介します。

fork

forkシステムコールは、親プロセスをコピーして子プロセスを生成します。
親プロセスのメモリ上のデータをそのまま子プロセスのメモリアドレス上にコピーします。
子プロセスはPIDなど一部の情報を除き、親プロセスと全く同じものがコピーされます。
詳細はman 2 forkWikipediaのforkに関する記事の情報が参考になります。

ちなみに、forkはC言語のライブラリ関数としても実装されています。
man 3am forkで仕様を確認できます (確認しなくて大丈夫です)。

forkライブラリ関数を呼び出すと、内部的にはforkシステムコールが呼ばれるという作りになっています。

clone

cloneシステムコールはforkと似ていますが、cloneの方が子プロセスに引き継ぐ情報をより細かく制御できるようです。
forkとcloneのユースケースの違いは理解していませんが、cloneもプロセスをコピーする処理なんだな、とここでは理解しておきます。

詳細はman 2 cloneに書いてあります。

execve

execveシステムコールは、呼び出したプロセス自身のメモリ上でプログラムを実行します。
結果として、execveを実行することで新たなプロセスに "変身" します。
man 2 execveのNOTESセクションにも書かれていますが、execveは新しいプロセスを生成しません。
また、execveの前後でPID (Process ID) は変わりません。
execveは別のアプリケーションを読み込み、同じプロセスの動作を置き換える命令です。

#ファイル実行時に呼ばれるシステムコールの繰り返しになりますが、ファイル実行したときはforkしてからexecveします。
forkで親プロセスから子プロセスをPID以外ほぼ丸ごとコピーし、execveで子プロセスならではの処理をインプットするという流れで処理が進みます。

ちなみに、execveにも関連するライブラリ関数があります。
man 3 execにexec系のC言語ライブラリ関数が列挙されていますが、これらの関数は内部的にexecveを利用しています。
(execl, execlp, execle, execv, execvp, execvpe)

実機確認

「ファイル実行はfork (またはclone) → execveの流れで進む」と説明しましたが、この情報に公式ドキュメントによる裏付けはありません。

ということで、2つだけですが実機ベースのログを添付することで、情報の確かさを補完したいと思います。

ファイル実行するとPPIDに実行元のPIDがセットされることの検証

bash上でpsコマンドを実行する例を考えてみます。

ps -fと入力してEnterキーを押しているのは私達人間ですが、実際に私達のキーボード入力を標準入力として受け取って命令を出しているのはbashプロセスです。

bashプロセスが/usr/bin/psファイルを実行してpsプロセスを起動しているので、psプロセスのPPIDにはbashのPIDがセットされるはずです。

このことは、ps -fを実行すればすぐに検証できます。
ps -fのPPID (Parent PID = 親プロセスのPID) が14429と、/bin/bashのPIDと一致していることがわかります。

ps -f
# UID      PID    PPID  C STIME TTY          TIME CMD
# endy   14429    5207  0 16:51 pts/2    00:00:00 /bin/bash
# endy   20072   14429  0 18:29 pts/2    00:00:00 ps -f

ファイル実行すると、fork (またはclone), execveの順に実行されることの検証

この検証では、straceコマンドを使います。

straceは指定したプロセスに接続し、そのプロセスに関わるシステムコールやシグナルを監視し、詳細なデバッグ情報を表示するツールです。
straceコマンドを実行するには、straceパッケージを事前にインストールしておく必要があります。

sudo dnf install strace

試しにpsプロセスをstraceしてみます。

strace ps -f
# execve("/usr/bin/ps", ["ps", "-f"], 0x7ffed16ab5e8 /* 60 vars */) = 0
# (以下略)

いきなりexecveを呼び出しているように見えます。
fork/cloneせずにexecveを呼び出すと、呼び出し元のbashがpsに置き換わってしまい、bashを維持できなくなってしまいます。
もちろん、そんなことはありません。

straceは、下図のようにbashプロセスがforkされて子プロセスが発行された後に、その子プロセスに接続して監視を開始しているようです。
したがって、上記の方法ではstraceでfork/cloneを観測することはできません。

straceでforkを観測するには、psの親プロセスであるbashを監視する必要があります3

strace_monitoring

では、上図の右側のやり方でfork/cloneを観測します。

まず、ターミナルを2つ起動しておきます。

1つ目のターミナルで現在実行しているシェルのPIDを確認します。
今回は8319だったとします。

※ちなみに、bashにおける$$、つまり$という特殊変数の意味は、man bashのSpecial Parametersセクションに書いてあります

echo $$
# 8319

続いて、2つ目のターミナルでPID 8319のプロセスをstraceで監視します。
-p <pid>で監視対象のプロセスを指定し、-fで子プロセスの情報も監視対象に含めています。
監視対象のbashから見てpsは子プロセスにあたるので、-fが必要となります。

strace -f -p 8319

1つ目のターミナルに戻り、psコマンドを実行します。

ps -f
# UID      PID    PPID  C STIME TTY          TIME CMD
# endy    8319    5207  0 Jan04 pts/1    00:00:00 /bin/bash
# endy   44850    8319  0 01:54 pts/1    00:00:00 ps -f

2つ目のターミナルにstrace結果が出力されるので、内容を確認します。
出力を抜粋したものを以下に示します。

# clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLDstrace: Process 44850 attached
# , child_tidptr=0x7f6d2616da10) = 44850

# (中略)

# [pid 44850] execve("/usr/bin/ps", ["ps", "-f"], 0x5573175278a0 /* 60 vars */) = 0

上記より、psを実行するとclone, execveの順にシステムコールが実行されていることがわかりました。
今回のケースでは、forkは呼ばれていませんでした。

まとめ

Linuxにおけるファイル実行時は、fork (またはclone)、execveシステムコールが呼ばれていることについて共有しました。

これからforkという単語が会話に出てきても、この知識でバッチリイメージできると思います。