1 はじめに
前の記事で、sudo rm -rf /*
した後に選択ソートを実装して、ユーザーが入力した数値列をソートしてファイルに書き出すということをやりました。
ps
コマンドが使えなかったこともあり、プロセス関連のことはほとんど触れられませんでした。というわけで今回は消え去ってしまったps
コマンドをbash組み込みコマンドのみから実装したいと思います。ちゃんと今回も最初からsudo rm -rf /*
します。
基本的に出てくるコマンドや構文は紹介をしますが、関数や変数の定義、配列、if文、for文など前回の記事で紹介したものは詳しく扱いません。前回の記事を参考にしてください。それではいきましょう。
・参考文献
オライリー・ジャパン 入門bash
・実行環境
Raspberry Pi 4 Model B+
Raspbian Buster Lite 2020-02-13-raspbian-buster-lite.img
2 下準備
sudo rm -rf /*
により消えてしまったコマンドはたくさんありますが、ls
とcat
はよく使うので再定義しておきます。また補完も変になっているので直しておきます。
2.1 lsコマンド
echo *
をls
のエイリアスとする(つまりalias ls="echo *"
とする)のでも良いですが、せっかくなのでディレクトリは青色、それ以外は白色で表示するls
コマンドを作ります。
そのためにはtest
コマンド([
コマンドでも同じ)を使います。このコマンドではファイル属性演算子を用いて条件判定を行います。例えば、-a
というファイル属性演算子は「そのファイルが存在するかどうか」を表します。具体例を見てみましょう。
/
において、proc
ディレクトリは存在するので終了ステータスとして0を返しますが、bin
ディレクトリはもう存在しないので1を返しています。
これを使ってls
コマンドを作ります。-d
はディレクトリが存在するかどうかを表すファイル属性演算子です。
function ls
{
for i in *
do
if [ -d $i ]
then
echo -ne "\e[34m$i\e[m\t" # ディレクトリは青色
else
echo -ne "$i\t" # それ以外は白色
fi
done
echo # 改行のためのecho
}
echo
で色をつけるのにこちらの記事を参考にしました。また、ファイル名の間にはtab文字(\t
)を入れています。このls
を適当なディレクトリで実際に使ってみましょう。
普通のls
と違って改行が少し変ですが、今回はよしとしましょう。ターミナルの表示列数を表すシェル変数COLUMNS
を使って場合分けすればもう少しきれいに出力できると思います。
2.2 catコマンド
これにはwhile文を使います。構文は以下です。
while < 条件式 >
do
< 一連の文 >
done
< 条件式 >
にはif文と同じように、コマンドの終了ステータスを用います。例えば以下です。
a=0
while [ $a -ne 3 ]
do
echo $a
let a++
done
-ne
は等しくない(not equal)を意味します。シェル変数a
が3以外のときは[ $a -ne 3 ]
は終了ステータス0を返すのでループし、a
が3になると1を返すのでループから抜けます。組み込みコマンドlet
は算術演算子を評価して、その結果を変数に代入します。上の例ではa
をインクリメントするだけです。
実際にやってみると以下のようになります。
a
が3になったときに、ちゃんとループから抜けてコマンドが終了していることがわかります。
それではwhile文を使ってcat
コマンドを実装します。簡単のため、引数を1つ取ってその中身を表示する機能のみを実装します。
function cat
{
while read val
do
echo $val
done
} < $1
read
コマンドは、シェル変数に値を代入するコマンドです。今回の例ではシェル変数val
にファイルの中身を1行ずつ代入して、echo
で表示します。$1
で指定したファイルの中身が空になった時、read
コマンドは終了ステータスとして1を返すので、ちゃんとループから抜けることができます。
2.3 補完について
また、sudo rm -rf /*
するとtabでうまく補完できなくなることがあります。
↓ tab入力
見たことないエラーが出ています。cat
コマンドにおいてtabの補完がどうなっているかは、組み込みコマンドcomplete
を使用することで見れます。
これはcat
コマンドの補完が_longopt
という関数で決められているという意味です。この関数の詳細はdeclare -f _longopt
で見れますが、ここでは深入りしないでおいておきます。要はこの関数がsudo rm -rf /*
した影響でうまく動かなくなっているということです。
今回はシンプルに補完をファイル名で行うようにしましょう。具体的には普通のファイル名から補完する-f
オプションを使って以下のように書きます。
complete -f cat
前の記事でも述べたとおり、ファイル名補完はESC+/
でもできるのですが、これでtabでもできるようになりました。ついでにcd
コマンドもtabが使えるようにしておきましょう。-d
オプションなら補完はディレクトリ名から行われます。
complete -d cd
3 psコマンドの作成
それでは実際にps
コマンドを作っていきます。まずはプロセスの情報を含む/proc
ディレクトリの説明から入ります。
3.1 /procディレクトリ
Linuxにおいては、プロセスの情報を含む/proc
ディレクトリというものが存在します。これは通常のディレクトリとは異なる擬似的なディレクトリで、sudo rm -rf /*
しても消えません。
実際に見てみると、こうなります。
上の方の数字のみからなるディレクトリには、そのプロセスID(PID)に対応したプロセスの情報が入っています。
ちなみにこの/proc
ディレクトリはLinuxでは存在しますが、純粋なUNIX(例えばBSD系のFreeBSDやMac OS、System V系のSolarisなど)では存在しない、もしくは存在しても中身が異なることがあるため注意です。
それではこのログインシェル(つまりbash)のプロセスIDのディレクトリを見てみましょう。現在のシェルのプロセスIDはecho $$
で調べることができます。
この中でstat
というファイルがあり、これにそのプロセスに関する情報が記述されています。
最初から見ていきましょう。
- 1つ目はプロセスIDで、値は
768
です。 - 2つ目がカッコで括られた実行形式のファイル名で、
(bash)
です。 - 3つ目はプロセスの状態で
R
です。R
は実行中(Running)を表しています。
全部は紹介しきれないので、詳細はman proc
で見てください。
これらの値を取得して、画面に表示するのがps
コマンドとなります。ただps
コマンドのデフォルトでは表示するプロセスが少なすぎるので、a
オプションをつけたps a
コマンド、つまり端末を持つ全てのプロセスを表示するps
コマンドを作りたいと思います。
まずはこのstat
ファイルを引数にとり、シェル変数に代入するread_stat
関数を作ります。
function read_stat
{
read -a values
pid=${values[0]}
comm=${values[1]}
state=${values[2]}
tty_nr=${values[6]}
time=$(( ${values[13]} / 100 ))
} < $1
まずはread
コマンドの-a
オプションを使って、values
という配列にstat
の各値を入れていきます。シェル変数の解説は以下です。
-
pid
プロセスID -
comm
実行形式のファイル名 -
state
プロセスの状態 -
tty_nr
プロセスの接続している端末名 -
time
実行時間(単位は秒)
実際に実行してみます。
ちゃんと値が入っているのが確認できます。
これで基本的な情報は表示できるのですが、端末名はうまく表示できません。それはtty_nr
に入っている数値(上の例では34816)をtty1
やpts/0
のような文字列に変換しないといけないためです。ここでデバイスファイルについて少し説明しましょう。
3.2 デバイスファイル
UNIX系のOSでは、HDDやUSBメモリ、端末といったハードウェアもファイルとして扱うことができます。このようなファイルをデバイスファイルと言います。出力を捨てるための/dev/null
やランダムな文字列を生成する/dev/random
もデバイスファイルで、皆さんも使ったことがあるかもしれません。デバイスファイルにはブロック型と文字型の2種類があります。前者はディスク装置を操作するためのファイルで、後者はそれ以外を扱うためのファイルです。
デバイスファイルはメジャー番号とマイナー番号を使って管理されています。実際のLinuxのドキュメントで確認してみましょう。
例えば「ブロック型デバイスファイルのメジャー番号1」は「RAMディスク」に割り当てられています。マイナー番号ではRAMディスクの何番目かを表します。
ps
コマンドで表示される端末は基本的にメジャー番号4のTTYデバイスか、メジャー番号136のUnix98 PTY スレーブです。TTYデバイスはラズパイに直接繋がっている画面、Unix98 PTY スレーブは擬似端末(pseudo terminal)の一種で、要はSSHで接続した時の画面です。
ではここで、tty_nr
に戻りましょう。この数値列の意味はman proc
に書いてあるので、引用してみます。
(7) tty_nr %d
The controlling terminal of the process. (The minor device number is contained in
the combination of bits 31 to 20 and 7 to 0; the major device number is in bits 15 to 8.)
tty_nr
の数値列の31から20ビットと7から0ビットの部分がマイナー番号、15ビットから8ビットの部分がメジャー番号であるということです。
これらを使って、シェル変数tty
に端末名を表示する関数を作成します。
function get_tty
{
major_num=$((tty_nr>>8))
minor_num=$((tty_nr&255))
if [ $major_num -eq 4 ]
then
tty=tty${minor_num}
elif [ $major_num -eq 136 ]
then
tty=pts/${minor_num}
else
tty=???
fi
}
メジャー番号は8ビット左シフト(tty_nr>>8
)を使うことで取り出し、マイナー番号は2進数で11111111である255との論理積(tty_nr&255
)から取り出しています。本当は31から20ビットも使わないと正確ではないですが、今回はこれでも問題ないのでこの形にしています。
メジャー番号が4なら普通の端末(tty1
とか)、136なら擬似端末(pts/0
とか)、それ以外は不明(???
)として、それにあったものをシェル変数tty
に代入しています。
実際に使ってみましょう。
この実験はmacからSSHで接続して行っているので、ちゃんと擬似端末が表示されました。
3.3 psコマンドの作成
ここまでに作ったread_stat
関数とget_tty
関数を用いて、ps
コマンドを作ります。
function ps {
echo -e "PID\tTTY\tSTATE\tTIME\tCMD"
for stat in /proc/[1-9]*/stat # 数字で始まりstatがあるディレクトリを考える
do
read_stat $stat
if [ $tty_nr -ne 0 ] # 端末を持つプロセスのみ表示
then
get_tty # ttyを取得
echo -e "${pid}\t${tty}\t${state}\t${time}\t${comm:1:-1}" # commの値の()は外す
fi
done
}
ここでシェル変数comm
は文字列がかっこ付きなので、それを取り外して出力(${comm:1:-1}
)しています。
なお、古いbashでは-1が使えないこともあるので、その場合は同じ意味である${comm:1:${#comm}-1}
を使うと良いと思います。
実際に使ってみましょう。
ちゃんと出力されました。今回はラズパイに直接つなげているモニターでもbashが起動しており、そのPIDが614ということもわかりました。
終わりに
bashの組み込みコマンドだけで、意外となんでもできるってことが実感できたと思います。時間があったら、次はプロセスの管理(ジョブの概念やkill
コマンド、シグナルなど)の話もやりたいと思います。