Web サーバには CGI をサポートし、外部のプログラムを実行できるようになっているものが多い。
今回はプログラム中から他のプログラムを実行する exec システムコールについて解説する。
exec() を使う典型的なプログラムは shell である。 Shell はコマンドとして入力されたプログラムを実行するのに、fork() によって新しいプロセスを作り、そのプロセス上で exec() を実行、そのプログラムを実行する。
↓ fork() ------- | | 親プロセス 子プロセス | | wait() exec() でプログラムを実行 : | : | : <---------- ↓
プログラムで書くと次のようになる。
pid = fork(); if (pid == 0) { /* 子プロセス */ exec(実行したいプログラムのパス名, ...); /* exec() はエラーでない限りリターンしない */ } else { /* 親プロセス */ int status; wait(&status); /* exec() で実行したプログラムが終了するのを待つ */ }
本格的な shell を作るためには、子プロセス実行中に、制御端末から ^C (強制中断) を打ったときに、shell ではなく、子プロセスを停止させなければならない。 このためには、子プロセス用に新しくプロセスグループを作り、制御端末をその新しいプロセスグループに入れなければならない。 詳しくは、setpgid(2), ioctl(2), termios(7) について調べよ。
次に exec() の具体的な使い方を説明する。 実は exec という名前のシステムコールはなく、引数の種類によって execl(), execv() などいくつかのシステムコールが存在する。
#include <unistd.h> int execl(const char* path, const char* arg0, const char* arg1, ...); int execv(const char* path, char* const argv[]);これらのシステムコールは path で指定されたプログラムを、指定された引数で実行する。 execl() の場合、第 0 引数、第 1 引数 ... がそれぞれ arg0, arg1, ... となる。 引数列の最後は NULL でなければならない。 execv() の場合、引数は配列の形で渡される。 なお、配列の最後の要素は NULL でなければならない。
使い方は例えば次のようである。
#include <stdio.h> #include <unistd.h> main() { int result = execl("/bin/ls", "ls", "-F", "/usr/local", NULL); printf("%d\n", result); perror("execl"); }
このプログラムは /bin/ls -F /usr/local を実行する。 第 0 引数は、通常、呼び出されるプログラムの名前であることに注意。 また execl() で実行されるプログラムは、shell を通さずに実行されるので、シェルスクリプトやホームディレクトリを表す ~ は使えない。
正常にプログラムを起動できたときには、execl() は永遠にリターンしない。 リターンするのは、プログラムの起動に失敗したときである。 上のプログラムでは、 ls の起動に失敗すると、execl() が -1 を返し、perror() でエラーの詳細が表示される。
exec システムコールは、プログラムを起動するときに、新しくプロセスを作るわけではない。 起動されるプログラムは、そのシステムコールを呼んだプロセスを使って実行される。 このためシステムコールを呼んだプログラムは、消去されてしまう。
exec システムコールは、そのシステムコールを呼んだプログラムを消去してしまうが、プログラムが open していたファイル・ディスクリプタは、新しいプログラムに引き継がれる。 どちらも同じプロセスで実行されるからである。 (OS はプロセスごとに、ファイル・ディスクリプタを管理していることを思いだしてほしい。)
Shell のリダイレクトやパイプといった機能は、この特徴を利用して実装されている。 例えば出力をファイル foo にリダイレクトする場合を考える。
この場合、shell は ls を execl() で実行する前に、ファイル foo を open() し、そのファイル・ディスクリプタが標準出力 1 になるように設定する。
pid = fork(); if (pid == 0) { /* 子プロセス */ fd = open("foo", O_CREAT | O_WRONLY, 0666); dup2(fd, 1); close(fd); /* fd は以後不要なので close() */ execl("/bin/ls", ...); } else { /* 親プロセス */ wait(); }
標準出力 1 を、open() したファイルに切り替えるシステムコールが dup2() である。 このシステムコールによって、以後、write(1, ...) はファイル foo への書きこみとなる。
パイプの場合、shell はふたつのプログラムを同時に実行するので、動作が多少、複雑になるが、基本はリダイレクトと同じである。 それぞれのプログラムを実行するプロセスで、標準入力 0 と標準出力 1 をそれぞれ、pipe システムコールで作成したストリーム(同一マシンの中でだけ使えるソケットのようなもの)の両端に切り替えればよい。
/* ls | wc の実行 */ int fildes[2]; pipe(fildes); if (fork() == 0) { /* 出力側子プロセス */ dup2(fildes[1], 1); close(fildes[0]); close(fildes[1]); execl("/bin/ls", ...); } else if (fork() == 0) { /* 入力側プロセス */ dup2(fildes[0], 0); close(fildes[0]); close(fildes[1]); execl("/bin/wc", ...); } else { /* 親プロセス */ close(fildes[0]); close(fildes[1]); wait(); wait(); }
pipe() を呼ぶと、fildes[0], fildes[1] にそれぞれ、作成したストリームの両端を表わすファイル・ディスクリプタが格納される。
exec() を利用して、これまで作成してきた web サーバでも外部のプログラムを実行して、その出力を web browser に返すことができるようにしよう。
いろいろな仕様が考えられるが、外部プログラムに引数を渡す必要がないのなら、実装は容易である。 GET 命令で指定されたファイルが、特定のディレクトリの下におかれている場合、そのファイルを execl() で実行し、出力をブラウザにそのまま送り返すようにすればよい。
例えば、外部のプログラムは ./bin ディレクトリの下におかれ、 web browser から
この仕様を実現するには、
を送信した後、もしファイル名が /bin/ で始まっていたら、ファイルの内容を送信するかわりに、そのファイルを execl() で実行し、プログラムの出力を web browser に送信するようにすればよい。
プログラムの出力を web browser に送信するためには、上で解説した shell のリダイレクト機能と同様のことをすればよい。
int fd = web browser と通信するためのソケットのファイル・ディスクリプタ; write(fd, "HTTP/1.0 200 OK\r\n\r\n", ...); dup2(fd, 1); close(fd); execl("./bin/a.out", "./bin/a.out", NULL); /* ./bin/a.out を実行する場合 */
ヘッダに続けて、a.out の出力が web browser に送信される。
Web サーバに引数を渡すには HTMLファイル中にフォームを埋めこむ。 例えば
のような入力をおこなうには、
<form method=POST action="bin/apply.cgi"> <p>名前: <input name="name" value=""> <p>性別: <input type="radio" name="sex" value="male" checked>男性 <input type="radio" name="sex" value="female">女性 <p><input type="checkbox" name="novice" value="yes">初心者 <p>感想: <textarea name="comment" rows=4 cols=60></textarea> <p> <input type="submit" value="登録"> <input type="reset" value="クリア"> </form>
などと書けばよい。
このフォームに対して、名前を "Chiba"、性別を男、初心者をオン、感想を "I'm very pleased with the talk." とし、 登録ボタンをクリックすると、ブラウザは
POST /bin/apply.cgi HTTP/1.0 Content-type: application/x-www-form-urlencoded Content-length: 72 name=Chiba&sex=male&novice=yes&comment=I%27m+very+pleased+with+the+talk.
のようなデータを web サーバに送信する。
送信されたデータは GET 命令ではなく、POST 命令である。 この命令はヘッダー情報の後に、情報(この場合はフォームの引数)を付加できる。
フォームの各項目に入力されたデータは、x-www-form-urlencoded と呼ばれる 形式に変換される。 各項目は <項目名>=<値> という形式に変換され、項目の間は & で区切られる。 感想 (commnet) の値については、空白は + に、非英数字は %<16進コード> という形式に置きかえられる。
POST 命令を受けとった web サーバは、付加情報を読み取り、適切な形に変換して外部プログラム (上の例では bin/apply.cgi) に渡さなければならない。 CGI (Common Gateway Interface, http://www.w3.org/CGI/) では、web サーバは読み取った付加情報を標準入力を経由してそのまま外部プログラムに渡すことになっている。 x-www-form-urlencoded 形式から、項目ごとの値を取り出す作業は、外部プログラムの仕事である。
なお、フォームを定義するときに、method として POST でなく、GET を指定することもできる。 GET が指定されると、ブラウザが送信するデータは次のようになる。
GET /bin/apply.cgi?name=Chiba&sex=male&novice=yes&comment=I%27m+very+pleased+with+the+talk. HTTP/1.0
POST 命令ではなく、GET 命令になり、フォームの引数は元々の URL の後に ? に続けて付加される。
CGI では、web サーバは引数部分を切り出し、環境変数
QUERY_STRING
を使って全体をそのまま外部プログラムに渡すことになっている。
exec システムコールを使って、課題 3 で作成した web server が外部のプログラムを起動できるようにせよ。
http://host1/bin/servlet のように、./bin ディレクトリの下にあるファイルが要求されたときは、servlet を実行し、その出力を結果として返すようにせよ。 ./bin 以外のディレクトリの下にあるファイルが要求されたときは、これまでどおり、そのファイルの内容をそのまま返すようにすること。
テストに使う外部プログラムとしては、 servlet.c を
% gcc -o servlet servlet.c
のようにコンパイルして用いよ。
Copyright (C) 1999-2001 Shigeru Chiba