プロセス
OSが実行ファイルを読み込んで実行する時、プロセスが新しく作られる。カーネルは、新しいプロセスを作るたび、各プロセスでどういった入出力が行われるかの管理テーブルを作る。そのインデックス値がファイルディスクリプタ。
ファイルディスクリプタに対応するものは通常のファイルには限らない。標準入出力(Stdin, Stdio)、ソケット、OSやCPUに内蔵されている乱数生成の仕組みまで、本来ファイルではないものにもファイルディスクリプタが割り当てられ、どれもファイルと同じようにアクセスできる。(ファイルのように扱うため、OSがファイルディスクリプタで抽象化)
ファイルディスクリプタはOSがカーネルのレイヤーで用意している抽象化の仕組みであり、カーネル内部のデータベースによって管理される。プロセスが起動されるとまず3つの疑似ファイルを生成し、それぞれにファイルディスクリプタを割り当てる。0が標準入力、1が標準出力、2が標準エラー出力。つまり、ファイルディスクリプタこれはプロセスの出入り口の参照表である。
以降はそのプロセスでファイルをオープンしたりソケットをオープンするたび、1つずつ大きな数値が割り当てられる?
POSIX系OSは可能な限りさまざまなものが「ファイル」として抽象化されている。
カーネルドライバなどでは実際にファイルディスクリプタを使ったシステムコールなどを行うが、Go言語では直接の手段は提供されておらず、少し上位のレイヤーで言語レベルで模倣しているio.Writer、io.Readerを使用することになる。プログラマーがOSのカーネルにシステムコールを実行(write)するように、Goはインターフェース(io.Write)にWrite()できる。
io.Readerやio.Writerといったインターフェースは、プロセスが外部との入出力に使うもの。それらのインターフェースの裏に、ファイルやソケットや標準入力、標準エラー出力などがある。
プロセスと外界のやり取りは全てシステムコールを介してOS経由で行う。ファイルやソケットからのデータ読み込み、現在時刻の取得など、全てシステムコール経由。プロセスが自分の力だけでできるのは、単純な計算くらいらしい。
プロセスは何らかのプログラムをメモリにロードし、それに対して実行するという形で起動される。
実行されたプロセスには次の情報が含まれる
実行ファイルパス
プロセスID
プロセスグループID、セッショングループID
ユーザーID、グループID
実効ユーザーID、実効グループID
カレントフォルダ
ファイルディスクリプタ
プロセスIDはプロセスごとにユニークなID。ほとんどのプロセスは既に存在しているプロセスの子プロセスとなっているので、親プロセスのIDなども取得できる。
プロセス間の関係は親子だけではなく、プロセスを束ねたグループというものがあり、プロセスはそのグループを示すID情報も持っている。パイプで繋がれて実効された仲間などは1つのプロセスグループ(別名ジョブ)と呼ばれる。プロセスグループIDは、グループに含まれるプロセスIDの代表のやつが使用される。
code:sh
cat sample.txt | echo
プロセスグループに似たものとして、セッショングループもある。同じターミナル(同じtty)から起動したアプリであれば、同じセッショングループになる。
プロセスは誰かしらのユーザー権限で動作する。また、ユーザーはいくつかのグループ(メイングループ1つ、サブグループ複数)に所属している。ユーザーとグループの権限はファイルシステムの読み書きの権限を制限するのに使われる。子プロセスを起動すると、その子プロセスは親プロセスのユーザーIDとグループIDを引き継ぐ。
プロセスのユーザーIDやグループIDは、通常は親プロセスのものを引き継ぐ。しかしPOSIX系OSえは、SUID、SGIDフラグを付与することで、実行ファイルに設定された所有者(実効ユーザーID)と所有グループ(実効グループID)でプロセスが実行されるようになる。(これらのフラグがない時は、元のユーザーIDとグループIDと同じ。)
これらのフラグが付与されている場合はプロセスの実効ユーザーIDと実効グループIDが変更される。
作業フォルダ(カレントフォルダ)もプロセスにおける大事な実行環境のひとつだ。作業フォルダはプロセスの中で1つだけ持つことが出来できる。マルチスレッドで動作するプログラムを作成しても、スレッドごとに別の作業フォルダを設定することはできない。
ファイルディスクリプタは先述のとおり、ファイルやソケットを抽象化した仕組み。どのリソースも「ファイル」として扱える。プロセスはこれらのリソースをファイルディスクリプタと呼ばれる識別子で識別する。カーネルはプロセスごとに、プロセスが関与しているファイル情報のリストを持っていて、ファイルディスクリプタはこのリストのインデックス値。(つまり、カーネルが持つファイルディスクリプタデータベースのインデックスをプロセスが持つ)
Dockerはシェルのコマンドはただのリモコンで、実体は管理者権限で動作するデーモンであるため、プロセスの親子関係はない。
---
プロセスには入力があり、プロセス内のプログラムがそれを処理し、出力がある。その意味ではプロセスは関数なものだとも言える。
全てのプロセスは3つの入出力データを持っている。プログラムによっては実行中にファイルや標準入出力の読み書き、ソケット通信など行ったりするが、この3つは必ずどのプロセスにも含まれている。
入力:コマンドライン引数
入力:環境変数
出力:終了コード
コマンドライン引数はコマンドに渡す引数である。(ARGなど)
プロセス終了時にはexit(0)関数などを呼ぶ。この引数は数値を取り、この数値がプログラムの返り値として親プロセスに渡される。そしてこの数値が終了コード。この数値は非負であり、慣習として0が正常、1以上がエラーとなる。
psやタスクマネージャーにはプロセスIDと一緒にアプリケーション名が表示されていたりするが、実はあるプロセスIDが何者であるかを知るすべは標準APIには存在しない。Linuxの場合、/procディレクトリの情報を取得してパースして表示している。
/procはカーネル内部の情報をファイルシステムとして表示した擬似的なディレクトリだ。/proc/プロセスID/cmdlineというテイストファイルの中にはコマンドラインの引数が格納されていたりする。
OSから見たプロセスとは、CPU時間を消費してあらかじめ用意しておいたプログラムに従って動く「タスク」。OSの仕事は、たくさんあるプロセスに効率よく仕事をさせることなど。Linuxではプロセスごとにtask_struct型のプロセスディスクリプタと呼ばれる構造体を持っており、プロセスを構成する全ての要素はこの構造体に含まれている。(先述したプロセスIDや、環境変数、引数なども)