慣れてくると当たり前になる概念も、しっかり説明しようとすると意外と難しいことがあります。
例えば、「Bash でコマンドを実行する」ということを説明しようとすると、実は結構難しいのではないでしょうか。
この記事では、「Bash でコマンドを実行する」ということを少しだけ掘り下げてみます。
- コマンドとは何か、コマンドをどうやって探すのか
- 実行可能なファイルとは何か、どんな流れで実行されるのか
という順で書いていきます。
まず一歩掘り下げる
掘り下げのまず一歩目として、コマンドを実行するときの基本的な概念について書いていきます。
プログラミングの入門などでも遭遇する話題で、なるべく早くに理解しておきたい内容です。
コマンドとは何か
例えば、Bash で
$ cat hello.txt world.txt
と入力して実行するとき、「cat」がコマンド名で、「hello.txt」と「world.txt」はそのコマンドに与える引数です。
引数という概念自体は、プログラミングで登場する関数の引数と同じなので、理解できている方が多いと思います。
では、「cat」のようなコマンドは一体なんなのでしょうか?
実は、Bash で実行できるコマンドには、「組み込みコマンド」と「外部コマンド」の 2 種類があります。
組み込みコマンドとは?
組み込みコマンドというのは、Bash 自体に実装されているコマンドです。
Bash のソースコードのどこかに書いてあるようなイメージで大丈夫です。
あるコマンドが組み込みコマンドかどうかは、type などで確認できます。
$ type alias
alias is a shell builtin
外部コマンドとは?
では、Bash に組み込まれていないコマンドである「外部コマンド」とはなんでしょうか。
外部コマンドの実体は、ただの「実行可能なファイル」です。
例えば cat は外部コマンドなのですが、以下のように特定のディレクトリに置いてあるただのファイルです。
$ type cat
cat is /bin/cat
$ ls -l /bin/cat
-rwxr-xr-x 1 root wheel 23648 5 4 2019 /bin/cat
この cat ファイルの中身は、ざっくり言うと機械語にコンパイル済みのプログラムです。
$ file /bin/cat
/bin/cat: Mach-O 64-bit executable x86_64
Bash は cat hello.txt world.txt
という入力を受け取ったとき、このファイルを探し出して実行しているのです。
ls コマンドも同じようにただのファイルですし、ruby コマンド、python コマンドなどもただのファイルです。
なので、例えば新しく php コマンドを実行できるようにしたい場合は、めちゃくちゃ単純化して言うと、php というファイルをダウンロードしてきて置いておけば OK です。
パス
といっても、たくさんディレクトリがある中でどこに置いてもいいということではありません。
それでは Bash が実行するファイルを探すときに困ってしまうのです。
ここで パス (PATH) という設定が関係してきます。
Bash が外部コマンドを探すときは、「パス」という設定に登録されたディレクトリを探します。
パスは echo $PATH
などで確認できます。
$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
もしパスがこのように設定されている場合、cat hello.txt world.txt
と入力すると、Bash は
- /usr/local/bin
- /usr/bin
- /bin
- /usr/sbin
- /sbin
というディレクトリを順番に見ていって、「cat」というファイルを探します。
そこで「cat」というファイルが見つかれば、それを実行してくれるのです。
1 ファイルで動くとは限らない
こういった仕組みで「cat」や「php」などの外部コマンドが実行されるわけですが、実は、「php」ファイルをダウンロードしてきて、パスが通った場所に置いておけば大丈夫とは限りません。
実行可能ファイルは、そのファイル 1 つで実行可能とは限らないのです。
実行時に必要とされるライブラリ (ダイナミックリンクライブラリ) は、例えば以下のように確認できます。
$ otool -L /usr/bin/php
/usr/bin/php:
/usr/lib/libresolv.9.dylib (compatibility version 1.0.0, current version 1.0.0)
/usr/lib/libcrypto.35.dylib (compatibility version 36.0.0, current version 36.0.0)
/usr/lib/libssl.35.dylib (compatibility version 36.0.0, current version 36.0.0)
:
このように、php コマンドひとつとっても、実は他のファイル (ライブラリ) に依存していたりするのです。
依存しているライブラリが不足していると、実行時にエラーになります。
この依存関係にあるファイルを手作業で 1 つ 1 つ探してダウンロードしてくるのは大変なので、yum や apt といった便利なツール (これもただの外部コマンド) を使ってインストールしたりするのです。
もう一歩掘り下げる
実行可能なファイルを実行するとはどういうことか、もう少しだけ掘り下げてみます。
ここからは初心者向けではなく、アプリケーションエンジニアであれば知らない方も少なくないと思います。
実行可能なファイルとは?
この記事の最初の方で、Bash は実行可能なファイルを探して実行すると書きました。
Bash で実行可能なファイルには
- バイナリ実行形式 (機械語のプログラム)
- シバン付きのスクリプト (#!/bin/bash や #!/usr/bin/ruby などで始まるスクリプト)
- シバンのない Bash スクリプト
の 3 種類があります。
Bash が実行可能なファイルを実行する流れ
Bash が実行可能なファイルを実行するとき、内部的には OS の中心である「カーネル」に対し、大きく 2 つの処理をお願いします。
1 つ目は、Bash の子プロセスを作ること (fork) です。
2 つ目は、作った子プロセスで、コマンドを実行すること (execve) です。
ちなみに、このようにカーネルに処理をお願いする方法を「システムコール」と言います。
execve での実行の流れ
Bash は、fork というシステムコールで子プロセスを作ったあと、execve というシステムコールを使って、カーネルにファイルの実行を依頼します。
execve は、
- バイナリ実行形式 (機械語のプログラム)
- シバン付きのスクリプト (#!/bin/bash や #!/usr/bin/ruby などで始まるスクリプト)
のどちらでも実行してくれます。
バイナリ実行形式 (機械語のプログラム) の場合は、そのまま実行していきます。
シバン付きのスクリプトの場合は、シバンで指定した実行可能なファイルに、Bash 上で指定したファイルを引数として与えらて実行します。
参考
シバンのない Bash スクリプトはなぜ実行されるのか
シバンのない Bash スクリプトは、execve では実行されません。
execve が失敗した後、Bash がテキストファイルなのかを判断した上で実行することになります。
ちなみに、このときは fork によって子プロセスが作られることはなく、既存の Bash プロセス上で実行されます。
参考