医療情報学
システム工学
UNIX, X Window 環境の概念や小技
ホーム / UNIX, X Window 環境の概念や小技
* 更新情報
  • 2006.04.18 02:00 - 最小主義者、端末、シェル

オープンソース化されたバージョンの UNIX (Linux や BSD)が普及してきました。以前であれば UNIX は大学の情報系の学科の占有物とすら言える状態でしたが、今では一般の人にも触れることができます。一方では、以前であれば UNIX が主たる計算機環境だった大学では UNIX についての教育に重きを置かなくなっているようです。

ここでは、UNIX, X Window を使う上で必要となる概念や小技について解説をします。

UNIX の UNI は UNIX が意識した Multics というOSへのアンチテーゼです。Multics は学会と企業が共同でつくった1960年代の次世代OSで、沢山の機能。つまり、マルチな機能を売りにしていました。しかし、そのプロジェクトは技術的には成功しましたが、Multics が普及することはありませんでした。それをあざ笑うかのようにたった二人の研究者がシンプルなOSを作り上げ、あっという間に普及しました。それが UNIX です。

UNIX は OS が提供すべき最小限の機能は何であるか、を追及しました。Multics は要求される機能があれば、その数だけ機能が実現されるというものですが、UNIX は要求される機能を実現するために最低限必要な機能は何であるか、というのがテーマです。何しろたった二人でC言語のコンパイラからOSのカーネルからデバイスドライバ、シェルやコマンドま作り上げるのですから無駄なものを作る余裕はありません。

UNIX には最小限の機能しか提供されていませんので、利用者は最小限の機能を組み合わせて目的を達成することになります。つまり、UNIX を利用するということは、事実上プログラミングをしているわけです。Windows のように要求される機能には対応する機能が提供されていて、それを選ぶことが操作すること、というのとはずいぶん違う世界です。

プログラミングという行動を身につける上では、普段の操作全てがプログラミングである UNIX に慣れることは、実力をつける上で有利です。

OS の上でプログラムを動かす方法はいくつかありますが、最低でもユーザーからプログラム起動要求を受け付ける、プログラムが必要です。そのようなプログラムは OS と一緒にインストールされていて、一般的にシェルと呼ばれます。シェルについて次項で書きます。

シェルとユーザーとの対話の方法には、グラフィック・ディスプレイとポインティング・ディバイスを多用するグラフィカル・ユーザ・インタフェイス(GUI)と文字のシーケンシャルな入出力によるコマンドライン・ユーザー・インタフェイス (CUI) スがあります。どちらにもネットワークを通してリモートから操作する方法が用意されています。

ユーザーインタフェイスを動かすハードウェアが、計算機(CPUとメモリが入ったメインの箱)とは独立したハードウェアとして実現されている場合、それを端末 (Terminal) と呼びます。簡単な機能の端末であれば、CPU が入っていない場合もありますが、いまどきではどんな端末でもある程度の計算能力が必要となるので、ターミナル自体も計算機です。

端末の機能を汎用の計算機の上で実現する為のソフトウェアもターミナルと呼ばれます(なぜか、ソフトの場合は端末ではなくターミナルと呼ばれる)。

端末と計算機の間の接続は普通はケーブル(規格としては RS-232C が使われることが多い)でつながれます。しかし、ネットワーク技術の進歩で直接的物理的な接続ではなく間接的論理的なネットワーク経由の接続も可能になりました。それらは仮想端末 (Virtual Terminal) と呼ばれます。いまでは、こっちの方が普通です。

Windows にも UNIX にも GUI、CUI 両方のシェルがあります。しかし、Windows では GUI シェルが中心。UNIX の場合は CUI シェルが中心です。

Windows のシェルは Explorer です。UNIX 上のシェルはプログラムの名前自体が sh (shell の頭2文字)です。sh は一番基本的なもので、普通は各種の機能拡張が施されたシェルが使われます。

Explorer と sh の一番の違い以下の二つの点です。

文法:
Explorer は目的物[、操作先]、操作の順で指定します。選択して右クリックでメニューを出して操作ですね。或いは、右ドラックでドロップ時にメニューがでますね。
sh は操作、目的物[、操作先]の順で指定します。[copy コピーするファイル コピー先のディレクトリ]ですね。

機能:
Explorer はファイルシステム上での操作は自前で行います。あとは、ファイル型(拡張子)に関連づけられたプログラムを起動します。
sh は実際に操作を行う機能はほとんど持っておらず、ほとんどの操作では別のプログラムを起動します。その代わり簡単なプログラミング用の制御構文や変数が使えます。

まず、基本的な癖をつけておいてください。必ず、コマンドラインのオプションにはどんなものが有るか確認しましょう。コマンドにオプションをつけないで起動するか、あるいは--help を付けて起動してみましょう。

例えば、(GNU) C コンパイラの場合、何も引数を付けないで起動すると

mobius1@flanker$ gcc
gcc: No input files specified
mobius1@flanker$

と、入力ファイル(Cのソース)の指定が無いと文句を言われます。そこで、一つでもソースコードを指定してあげます。

mobius1@flanker$ gcc helloworld.c
mobius1@flanker$

と黙って終ります。コンパイルの結果、実行可能ファイル a.out が出来上がっています。

一方、何もコマンドラインオプションを指定しないと、止まってしまうプログラムもあります。

mobius1@flanker$ cat

これは、止まっているけど、別にフリーズしたり無限ループに入っているのではなくて、標準入力からの入力を待っているのです。UNIX 上の文化として、標準入力をデフォルトの入力とするプログラムは、コマンドラインオプションを指定しないと標準入力を待って止まります。

一方、標準入力をデフォルトの入力としない(できない)プログラムは、コマンドラインオプションが無いと、先の gcc のように文句を言うか、javac の様に使い方(Usage)を教えてくれます。

mobius1@flanker$ javac
Usage: javac <options> <source files>
where possible options include:
  -g                         Generate all debugging info
  -g:none                    Generate no debugging info
  -g:{lines,vars,source}     Generate only some debugging info
  -nowarn                    Generate no warnings
  -verbose                   Output messages about what the compiler is doing
  -deprecation               Output source locations where deprecated APIs are used
  -classpath <path>          Specify where to find user class files
  -cp <path>                 Specify where to find user class files
  -sourcepath <path>         Specify where to find input source files
  -bootclasspath <path>      Override location of bootstrap class files
  -extdirs <dirs>            Override location of installed extensions
  -endorseddirs <dirs>       Override location of endorsed standards path
  -d <directory>             Specify where to place generated class files
  -encoding <encoding>       Specify character encoding used by source files
  -source <release>          Provide source compatibility with specified release
  -target <release>          Generate class files for specific VM version
  -version                   Version information
  -help                      Print a synopsis of standard options
  -X                         Print a synopsis of nonstandard options
  -J<flag>                   Pass <flag> directly to the runtime system

prprt@flanker$

明示的に助けを求める場合は、コマンドラインオプションに --help を付けます。

mobius1@flanker$ gcc --help
Usage: gcc [options] file...
Options:
  -pass-exit-codes         Exit with highest error code from a phase
  --help                   Display this information
  --target-help            Display target specific command line options
  (Use '-v --help' to display command line options of sub-processes)
  -dumpspecs               Display all of the built in spec strings
  -dumpversion             Display the version of the compiler
  -dumpmachine             Display the compiler's target processor
  -print-search-dirs       Display the directories in the compiler's search path
  -print-libgcc-file-name  Display the name of the compiler's companion library
  -print-file-name=<lib>   Display the full path to library <lib>
  -print-prog-name=<prog>  Display the full path to compiler component <prog>
  -print-multi-directory   Display the root directory for versions of libgcc
  -print-multi-lib         Display the mapping between command line options and
                           multiple library search directories
  -print-multi-os-directory Display the relative path to OS libraries
  -Wa,<options>            Pass comma-separated <options> on to the assembler
  -Wp,<options>            Pass comma-separated <options> on to the preprocessor
  -Wl,<options>            Pass comma-separated <options> on to the linker
  -Xassembler <arg>        Pass <arg> on to the assembler
  -Xpreprocessor <arg>     Pass <arg> on to the preprocessor
  -Xlinker <arg>           Pass <arg> on to the linker
  -save-temps              Do not delete intermediate files
  -pipe                    Use pipes rather than intermediate files
  -time                    Time the execution of each subprocess
  -specs=<file>            Override built-in specs with the contents of <file>
  -std=<standard>          Assume that the input sources are for <standard>
  -B <directory>           Add <directory> to the compiler's search paths
  -b <machine>             Run gcc for target <machine>, if installed
  -V <version>             Run gcc version number <version>, if installed
  -v                       Display the programs invoked by the compiler
  -###                     Like -v but options quoted and commands not executed
  -E                       Preprocess only; do not compile, assemble or link
  -S                       Compile only; do not assemble or link
  -c                       Compile and assemble, but do not link
  -o <file>                Place the output into <file>
  -x <language>            Specify the language of the following input files
                           Permissible languages include: c c++ assembler none
                           'none' means revert to the default behavior of
                           guessing the language based on the file's extension

Options starting with -g, -f, -m, -O, -W, or --param are automatically
 passed on to the various sub-processes invoked by gcc.  In order to pass
 other options on to these processes the -W<letter> options must be used.

For bug reporting instructions, please see:
<URL:http://gcc.gnu.org/bugs.html>.
prprt@flanker$ cat --help
cat: illegal option -- -
usage: cat [-benstuv] [file ...]
prprt@flanker$ javac --help
Unrecognized option: --help
Could not create the Java virtual machine.
prprt@flanker$

--help というのは、POSIX という IEEE が定めた UNIX 系オペレーティングシステムの標準規格の中で、使い方等を表示させる為のオプションとして定められています。

良く見ると、cat コマンドは、--help を理解して使い方を表示しているのではなく、おかしなオプションを付けられたので「やれやれ、ワシらの好物も知らずにチョッカイをだす人間が未だにいるとは...ほれ、猫の好物はこれじゃ(なんか、cat=宮崎アニメの化け猫のような感じ。でも実際 cat の歴史は古く、少なくとも私と同じか上)」という感じで使い方を表示してきます。猫の癖に!とムカツキますが、何であれ、こちらからすれば目的は達成するのでよしとしましょう。

一方、POSIX が気に入らない Sun が作った Java は --help には対応しておらず、-help を求めてます。

所で、使い方説明の中で <xxx> という項目が頻繁に出て来ます。これは、その文字列をいれるのではなく、ファイル名とかユーザー名、行番号、URL とかをいれろという意味です。要するに変数みたいなものです。<、 > で囲むのは、これらがシェルのリダイレク記号なので、これをコマンドオプションには絶対に出来ないから、混同する心配が無いからです。

一方、[xxx] となっているものが有ります。これは、それが省略可能だという意味です。正規表現とかBNFとか知っていますよね?

自分でプログラムを作るときには、この文化を守るようにしましょう。また、GNU では getopt というライブラリを用意して、コマンドラインの解釈をいちいち毎回書かなくても良いようすると同時に、文化の保全に寄与しています。

UNIX や Windows などの VAX/VMS の流れを引く OS には環境変数という仕組みが有ります。

環境変数は、プロセス毎に1セット持っています。在処は実用的プログラミングの基礎知識/メモリ管理の図1の envp の所に置かれます。

だれが設定するのでしょうか? 置き場所を確保してくれるのは OS ですが、OS は値を設定してくれるわけでは有りません。これを理解するには、UNIX のプロセス/プログラムの起動の概念を理解する必要が有ります。それは、別の項目で説明するとして、ここでは、設定するための方法をお教えします。

シェルの環境変数を設定する(sh, bash)
mobius1@eagle$ export LANG=ja_JP.eucJP
シェルの環境変数を設定する(csh, tcsh)
mobius1@eagle% setenv LANG ja_JP.SJIS

シェルの環境変数は変えずに、違う環境変数を与えてプログラムを起動する(env コマンドを使う)
mobius1@eagle$ env LANG ja_JP.UTF-8 firefox

環境変数の値を表示する方法は

mobius1@eagle$ printenv LANG
ja_JP.eucJP

あるいは、

mobius1@eagle$ echo ${LANG}
ja_JP.eucJP

後者は環境変数をシェル言語の中で使う(参照する)やり方を示しています。つまり環境変数名を ${...} で囲む(頭に $ をつけるだけでも良いです)とその値を取り出せるわけです。設定するときには $ がいらず、読み出す時には $ がいる...なぜなのかでしょうか。古狐の私でさえ知らない秘密だ。

環境変数が envp の中でどういう風に格納されているかと言えば、

名前1=値1\0.....\0名前N=値N\0\0

という形で、ヌルターミネートでビッチリ連結されて格納されいて、最後はヌルヌルターミネートです。

計算機上で日本語を使う際に常に悩ませ続けられるのが文字コードです。

Web ブラウザや高機能なエディタは自前で文字コードに対する処理をしてくれますし、テキストを読み込む際に文字コードの認識を自動的にやってくれます。

全く日本語を扱えないプログラムはしょうがないとして、一応対応しているプログラムに対しては、利用者がどの文字コードを使っているかを指示してやらなければなりません。

その為に LANG という環境変数を使います。LANG 環境変数が設定されていない場合、プログラムは共通語(Common の意味で C、つまり英語)が使われているとして処理します。日本語系統では、演習機の上では次の値が使えます。

  • ja_JP.SJIS
    日本語 文字セット・シフトJIS エンコーディング
  • ja_JP.UTF-8
    日本語 文字セット・UTF-8 エンコーディング
  • ja_JP.SJIS
    日本語 文字セット・EUC エンコーディング

ctwmtwm に機能追加したものです。twm は X Window の標準ウィンドウマネージャで、Windows ユーザーからすると操作が全く違ってびっくりします。

ルートウィンドウ(背景、実はここもウィンドウです)上で左クリックで、アプリケーション起動メニューが現れます。

ルートウィンドウ上での中クリック(ボタンが二つのマウスの場合は右左同時押し)で、ウィンドウ操作メニューが現れます。メニューを選択すると、ウィンドウ操作モードに入りマウスカーソルの形が変わます。このモードで操作対象のウィンドウを左クリックすると、選択した操作が実行されます(オブジェクト指向ではありません)。右クリックをするとウィンドウ操作モードはキャンセルされます。

ウィンドウのボーダーをドラッグするとウィンドウのサイズ変更ができます。Windows ユーザーが戸惑うのは、かならず、一旦、広げる方向に動かす必要があるということです。

ウィンドウのタイトルバーをドラッグするとウィンドウの移動。これは普通。

マウスがあるウィンドウのボーダーは赤くなりフォーカスされていることを示します。キーボードの入力はフォーカスされているウィンドウに入ります。Windows と違うのは、クリックしなくてもマウスを移動するだけで、フォーカスされるウィンドウが変わると言うことです。また、フォーカスされているウィンドウが必ず最上位に上がって来るわけではないというのも、Windows と違います。

ウィンドウ(の重なり)を最上位にするには、ウィンドウのタイトルバーかボーダーを左クリックします。

タイトルバーの左端はアイコン化ボタンです。

タイトルバーの右端はリサイズアンカーです。twm ではここ以外ではウィンドウのリサイズができません(その分、ウィンドウボーダーが無い)。ctwm はウィンドウボーダーを付けたので、どこでもリサイズができます。

仮想デスクトップの選択パネルがあります。クリックすると仮想デスクトップを切替えられます。狭いモニターでも同時に沢山の仕事がしやすくなります。自分なりに使い方を考えてください。

新しいウィンドウが現われるとき(アプリケーションを起動したり、ダイアログを出したり)、ウィンドウの位置を聞いて来ます(ワイヤー表現)。左クリックで確定します。右クリックすると、その位置から下側に最大化します。

これだけ覚えればとりあえずは作業はできます。ここから先は、.ctwmrc を編集して自分の使いやすいようにカスタマイズすることになります。

X Window は GUI システムとしてはは歴史が古く、また、クリップボードの使い方がオリジナルのまま残っているため、人気の新参者の Macintosh 系(Windows も GNOME も真似をした、Modifier-X/C/V のキーを使う操作)と全く違います。ただし、最近のアプリケーション (GNOME や KDE) では Mac 系の操作も併用できるようになってきています。

コピー: 選択(ハイライト)しただけでコピーされます。クリップボードは一つしかありませんから、複数のウィンドウを操作しているときに一つのウィンドウで選択をすると他のウィンドウのハイライトは解除されます。ただし、たまにこのハイライト解除が効かずに複数のウィンドウにハイライトが残ってしまう場合があります。そういう場合は、改めて選択した方が無難です。

ペースト: 中クリック(2ボタンのマウスでは左右同時押し、ホイールマウスではホイールがボタンになっています)

カット: ありません

cygwin/X のクリップボードは Windows のクリップボードと互換性があります。Windows 上でコピーして、cygwin/X に切替えてペーストしたり、その逆もできます。

注意:X Window のクリップボードはテキストしかサポートしていません。画像や書式付きテキスト等の複雑なデータの扱いはアプリケーション毎に違うため、通常はアプリケーション間でのクリップボード操作はできません。ただし、GNOME や KDE 等のデスクトップ環境では X Window のクリップボードに加えて自前のクリップボードシステムを持っているので、、異なったアプリケーション間でのクリップボードを実現しています。

日本語の入力の仕方は、カナ漢字変換エンジンやアプリケーションの種類によってまちまちです。Free Choice の UNIX では仕方の無いことです。

演習機ではカナ漢字変換エンジンとして Canna (http://canna.sourceforge.jp/)を用いています。

演習機で日本語入力ができるアプリケーションは Emcws と GEdit です。それぞれ使い方が違います。

Emcws での日本語入力

日本語入力の開始/終了は Control-o です。

GEdit での日本語入力

GEdit を含む GNOME 系アプリケーションの日本語入力は、日本語入力ライブラリ im-ja (http://im-ja.sourceforge.net/)によって実現されています。

日本語入力の開始/終了は、Shift-Space です。

画面のスナップショットの取り方ですが、方法は二つあります。一つは、cygwin/X だからできる方法です。もう一つは X Window の正しいやり方です。

cygwin/X は実はフルスクリーンモードで走っているにしても、所詮は Windows のアプリケーションだという事です。つまり、[Print Screen] キーを押せば、画面(全画面)がクリップボードに記録(コピー)されます。あとは、それを画像が扱える適当なソフト(ペイントでいいと思いますが)にペーストすればいいのです。

X Window の正しいやり方と言うのは、X Window に標準としてバンドルされている xwd というコマンドつかう方法です。これは、man ページで調べてください...ではちょっと難しいかな?

mobius1@foxhound$ xwd -root -silent -out screen.xwd
mobius1@foxhound$ ls -l screen.*
total 7520
-rw-r-----  1 mobius1  mobius1  7683179 Oct 19 09:27 screen.xwd
mobius1@foxhound$ convert screen.xwd screen.png
mobius1@foxhound$ ls -l screen.*
total 8736
-rw-r-----  1 mobius1  mobius1  1217368 Oct 19 09:30 screen.png
-rw-r-----  1 mobius1  mobius1  7683179 Oct 19 09:27 screen.xwd
mobius1@foxhound$ 

xwd というのが画面をキャプチャーするコマンド。convert というのは画像ファイルの形式を変換するコマンド(ImageMagick)です。

FreeBSD のマスコットきゃらのデーモン君(悪魔ではなく精霊です)が持っている三叉の槍。これは UNIX のプロセスの動きを表したものなのです。

実は、UNIX のシステムコールには、プログラムを起動する、というのは無いんです。有るのは、fork()exec() の二つ。exec() ってプログラムの起動のように思ってしまいますが、全然違います。

fork() システムコールは、それを呼んだプロセスのコピーを作ります。例に、下記のプログラムを FreeBSD 上で実行してみてください。

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char** argv)
{
    pid_t ppid = getpid();
    printf("I am pid = %d. Let\'s go !\n", ppid);
    if (fork() == 0) {
        printf("I am pid = %d, child of %d\n", getpid(), ppid);
        return 2;
    } else {
        int result;
        pid_t cpid = wait(&result);
        printf("I am pid = %d. My child (%d) exits %d\n", ppid, cpid, WEXITSTATUS(result));
        return 1;
    }
}

実行すると、

I am pid = 80386. Let's go !
I am pid = 80387, child of 80386
I am pid = 80386. My child (80387) exits 2

とプリントされます。二つに分かれる様が食器のフォーク(あるいは、三叉の槍。ただ、こいつらは二つ以上に分かれていますが...。フォークの名前を選んだ理由は実はもう一つ有って、ダイクストラがマルチタスクの説明に用いた「食事をする哲学者」で哲学者がスパゲティを食べるためにフォークを使うところからです。)の形に似ているからです。メモリの中身(コード、ヒープ、スタック)は勿論、ファイルディスクリプタや環境変数、そして CPU のレジスタ(一部はどうしても違ってしまいますが。例えばプログラムカウンタとかセグメントレジスタとか)までコピーされます。

一方、exec() は新しいプロセスは起動せず、指定された実行プログラム(のイメージ)を exec() を呼び出したプロセスに読み込みます。当然ですが、exec()というシステムコールからは帰って来ません。だって、exec() の実行が終了した時点で、そのプロセスの中身はすっかり入れ替わってしまいます。もし帰って来たらそれは exec() に失敗したということです。

そして、exec() の憎いところは、ファイルディスクリプタや環境変数は上書きせずに残します。これらのリソースはプログラムの一部ではないからです。

このシンプルな仕組みの絶妙な組合せによって、UNIX は非常に多様な動きを実現することが出来るのです。とくに顕著なのは | (パイプ) です。

いわゆる「プログラムの起動」というのは、シェルとかウィンドウマネージャが自らを fork() したあと、指定されたプログラムを exec() しているのです。

シェルもどき(このままではちゃんとは動きません)
int main(int argc, char** argv)
{
    char* prog = argv[1]; // 最初のコマンドライン引数をプログラムファイルとして実行します。
    char* back = argv[2]; // バックグラウンドに送るかどうか。'&' でチェック

    if (fork() == 0) {
        // ここは子プロセス
        exec(prog);
        // 子プロセスの中身は別のプログラムファイルによって上書きされ別人に...
        // でも、ファイルディスクリプタや環境変数という資産は引き継いでいる。
        // でも、子プロセスは自分の親プロセス(のID)を知る術を持っていない...
        // ...どういう親子じゃ...
    } else {
        // ここは親プロセス(子を持ったから親になった!)
        if (*back == '&') {
            // バックグラウンドに送るということは、子プロセスの終了を待たないということ。
        } else {
            // バックグラウンドに送らない、ということは、子プロセスの終了を待つと言うこと。
            int result;
            pid_t cpid = wait(&result); // 子プロセスの終了を待つシステムコール
        }
    }
}

※ アセンブラ(機械語)やCPUの動作(特に割り込み)について知識のあるひとは、マルチプロセスがどうやって実現されるか、自分で考えてみてください。面白いですよ。教官は OS は書いたこと無いですが、マルチタスクの仕組みをアセンブラで作ったことが有ります(プログラミング経験5年目)。そう、実はマルチタスクの仕組みを作ることは自体はそんなに難しいことじゃないです。それよりは、マルチタスクを活用したアプリケーションを作る方がずっと難しいです。

「ファイルを開く」というのはどういうことでしょう?通常はファイルの内容をメモリに読み込むことですね。メモリに読み込んだ後は、プログラムによってやることがちがいますが、ファイルの内容をメモリに転送する、という動きは共通です。共通の動きを何度も作るのは面倒ですので、誰かが良く考えられた使いやすい仕組みを作っておいてくれると助かります。OS は全てのプログラムの動きを支える基盤ですので、OS がそのような共通の仕組みを持つべきです。実際、どんな OS もファイルの内容をメモリに転送する仕組み(当然、その逆も)を持っています。歴史が UNIX より古い汎用機は独自の仕組みをもっていますが、UNIX 以降に登場した OS はみんな UNIX の仕組み(実際には Multics が下敷になっているのですが)に習っています。その仕組みとは...

UNIX はファイルの管理とファイルの中身へのアクセスを全く別の仕組みで処理するようにしました。ファイル(ディレクトリも特殊なファイルです)の管理はファイルの名前で管理します。一方、ファイルの中身にアクセスする際にはファイル・ディスクリプタというデータを OS 内部のメモリに持つようにしました。なぜそうしたのか、それは、ファイル以外のもの(シリアル接続(モデムとか)、パラレル接続(プリンタとか)、ネットワーク)も同じように扱えるようにしたかったからです。

ファイル・ディスクリプタというデータは OS 直轄のメモリの中にあります。メモリの中に有るわけですから、OS が走っていない(まぁ、普通は電源が入っていない状況ということになりますが)時には存在しません。一方、ファイルはディスクの上に有りますから電源のオンオフは関係ないですね。OS 直轄のメモリの中は、プログラムからは絶対に触れません。そこで ファイル・ディスクリプタ を操作するためのシステムコールが用意されました。重要なものとしては、open()close()read(), write() などです。

open() システムコールは、名前で管理されているファイルと、新たに作ったファイル・ディスクリプタを結びつけます。これで「ファイルが開いている状態」になるわけです。

さて、プログラムは ファイル・ディスクリプタ に直接はアクセス(ファイル・ディスクリプタ構造体へのポインタを入手してメンバー変数等にアクセス)することは出来ないです。そこで、open() システムコールはポインタの代りに一意の番号を int で返します。以降、ファイル・ディスクリプタ にアクセスする為のシステムコールにはこの番号を引数として与えることで、アクセスしたい ファイル・ディスクリプタ を識別します。

cat もどき
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char** argv)
{
    for (int i = 1; i < argc; i++) {    // 引数全部を処理します
        char buf[4096];
        int fd = open(argv[i], O_RDONLY);    // 引数をファイル名としてファイルを開きます。
        ssize_t count;
        while ((count = read(fd, buf, sizeof(buf))) > 0) { // 読み込み
            write(1, buf, count);
        }
        close(fd);
    }
}

ここで、write() の引数として、1 という定数を与えていることに注目してください。

実は、プロセスが起動されると、3つの ファイル・ディスクリプタ が既にできています(実際には、最初のプロセスのために OS が作ってあげて、後は fork()/exec() でそのままコピー/継承しているだけですが)。この三つが、標準入力(0)、標準出力(1)、標準エラー(2)です。

ちなみに、ファイル・ディスクリプタ の番号は使い回しされます。つまり、close() の直後に open() すると同じ番号が割り当てられます。これは実装上の偶然ではなく仕様で、これがシェルの "|" (パイプ)実現のキーなのです。

UNIX のシェルの上では頻繁に "|" (パイプ)を使います。というか、パイプを使わないなら、UNIX を使う意義は全く有りません。パイプは pipe() システムコールが基盤になっています。

動くけど、あんまり意味のない pipe() の使用例
#include <unistd.h>

int main(int argc, char** argv)
{
    int fds[2];
    pipe(fds);                                   // パイプを作る

    write(fds[1], "hello, world\n", 13);         // パイプに書き込む

    char buf[80];
    int count = read(fds[0],  buf, sizeof(buf)); // パイプから読み出す

    write(1, buf, count);                        // 結果を標準出力に出す

    close(fds[0]);
    close(fds[1]);
}

pipe() システムコールを呼ぶと、実体(ファイルとかネットワークとか)の無い空箱で、その両端にファイル・ディスクリプタがついている、パイプというものが作られます。上の例では、片方のファイル・ディスクリプタから書き込んで、反対側のファイル・ディスクリプタから読み出しています。これは、素直に動きます。では、もっと一度に沢山、パイプに流し込んでみましょう。

止まりやすい pipe() の使用例
#include <unistd.h>

int main(int argc, char** argv)
{
    int fds[2];
    pipe(fds);

    char buf[80];
    int count;

    while ((count = read(0, buf, sizeof(buf))) > 0) { // 標準入力(0) から読み込み、
        write(fds[1], buf, count);                    // どんどんパイプに流し込む
    }

    while ((count = read(fds[0],  buf, sizeof(buf))) > 0) { // どんどんパイプから読みだし
        write(1, buf, count);                               // 標準出力(1)に書き出す。
    }

    close(fds[0]);
    close(fds[1]);

}

このプログラムに(リダイレクトを使って標準入力から)大量のデータを流し込むと、途中で止まってしまいます。これは、write(fd[1], buf, count) でブロック(停止)されるからです。なぜかと言えば、だれもパイプからデータを読み出してくれないので、パイプが一杯になってしまい、もう書き込めないからです。

反対に小量を流し込んだ場合は、流し込んだ分を出力した後、止まってしまいます。これは、read(fd[0], buf, sizeof(buf)) でブロックされるからです。なぜかと言えば、空っぽになったパイプから更にデータを読み出そうとして、誰かがデータを書き込むのを待っている状態になってしまうのです。

この例から分かるように、一つのプロセスの中でパイプを使っても意味がなく、パイプに書き込むプロセスとパイプから読み出すプロセスの両方が同時に走っていないと、パイプの中をデータが流れないということです。

UNIX のシェルでは、コマンドの後に "&" をつけると、このコマンドをバックグラウンドで実行する指定になります(マルチプロセスのセクションを参照のこと)。実は、"|" は同じようにバックグラウンドで実行することを指定する記号なのです。"&" と違うのは、前のプロセスの標準出力と後のプロセスの標準入力をパイプを使って接続するのです。この仕組みはかなりトリッキーです。そのトリックの鍵は、fork(), exec() システムコールでは、ファイル・ディスクリプタ がそのまま温存されること、close() で ファイル・ディスクリプタ を閉じて次に open() を呼び出すと同じ番号が使い回されること(この場合は dup() を使う)、等です。自分がまだ UNIX をよく理解していない頃、この部分のコードを会社の後輩(今は某社の重役)に解説してもらったことがありますが、かなりトリッキーなものでした。今すぐに自分で考えろといわれてもすぐには思い付かないです。

シェルが二つのコマンドをパイプで繋いで起動すると、プロセスは三つに分かれます。一つはシェルそのもの、残りの二つはシェルが起動したコマンド。プロセスが三つの分かれる様がデーモン君の持っている三又の槍なわけです。