過去にPHPで以下のコードを実行した場合、このprint
の結果はどこに吐かれるんだろう、と混乱したことがあった。
<?php
print("Hello World");
CLIで実行すれば、制御端末のコンソールに表示されるんだろうけど、NginxやApacheのようにWebサーバー越しで実行している場合、あれーとわからなくなったのだ。
実際のところ記憶が曖昧だけど、HTMLとして出力されていたり、どっかのログに吐かれていたような気がする。
この疑問を解消しようと思ったとき、そもそもC言語で実装されているNginxやApacheでどうやってPHPを動かしているんだろうという部分に行き当たることになった。
ちょうど年末からLinuxでWebサーバーをお試しでつくってみようということをやっていたので、実装の参考にするべくApacheのソースコードを眺めつつ、まずはApacheから任意のプログラムを実行するCGIの仕組みについて調べることにした。
Linuxの基本調べるにあたっては、以下の書籍を参考にしています。
まずは結論
- 自プロセスを新しいプログラムで上書きして実行するAPIである
exec
を使う。
以下はApacheの該当部分のソースコード。
else if (attr->cmdtype == APR_PROGRAM) {
if (attr->detached) {
apr_proc_detach(APR_PROC_DETACH_DAEMONIZE);
}
execve(progname, (char * const *)args, (char * const *)env);
}
- 上記のexecはfork & execで実行されるので、毎回プロセスを生成する必要があり、その分のオーバーヘッドが大きい。
C言語からNode.jsを実行する
あれ?PHPは?と思ったけど、ひとまずC言語以外ならなんでもいいかと思い、気づいたらNode.jsをインストールしていた。
ということでC言語からNode.jsを実行してみる。
基本
実行するjsは以下のように文字列を標準出力に吐くだけのプログラム。
sample.js
console.log("hello");
これをC言語から呼ぶには、exec
族と呼ばれる関数を使う。
exec
族にはいろんな種類があるんだけど、ここではexecve
を使うことにする。
execve
は引数で指定したプログラムを自プロセスに上書きしてくれるLinuxのシステムコールで、他のexec族の関数はexecve
をラップしているライブラリ関数とのこと。
以下のように使うことができる。
main.c
void main() {
char *argv[3] = {"node", "sample.js", NULL};
execve("/usr/bin/node", argv, NULL);
printf("execveが成功しているのであれば、このコードは実行されない\n");
exit(1);
}
前述の通り、execve
は引数で指定したプログラムを自プロセスに上書きするので、execve
以降の処理は実行されることがない。
これを実行すると、以下のようにsample.js
の実行結果が標準出力に表示された。すごい。
$ ./exec
hello
forkと組み合わせる
これで目的の8割ぐらいは達成できたんだけれども、WebサーバーのCGIとして使う場合、Node.jsで処理した結果をHTTPのレスポンスとして返す必要がでてくる。
今のままだと、Node.jsの処理の実行が終わるとそのままプログラムが終了してしまうため、これに対応するためにfork
を使うことにする。
fork
は自身のプロセスをコピーして新しいプロセスをつくるLinuxのシステムコール。
execve
は任意のプログラムをプロセスに上書きするので、forkしてそれ用にプロセス一個つくっとこうという感じでしょうかね。
www.tohuandkonsome.site
forkと組み合わせると以下の通りになります。
void main() {
pid_t pid;
pid = fork();
if(pid < 0) {
fprintf(stderr, "fork(2) failed\n");
exit(1);
}
if(pid == 0) {
char *argv[3] = {"hoge", "sample.js", NULL};
execve("/usr/bin/node", argv, NULL);
printf("execveが成功しているのであれば、このコードは実行されない\n");
exit(1);
} else {
int status;
wait(&status);
}
exit(0);
}
子プロセスの結果を親プロセスで受け取る
最後に子プロセスで実行したNode.jsの結果を親プロセスで取得する。
ここでいうNode.jsの結果とは、console.log()
で標準出力に出力している文字列hello
を指している。
まずは、Node.jsで使う標準出力の出力先を任意のファイルに変更してみることにする。
これはdup
関数を使用することで実現できる。
void main() {
pid_t pid;
int fd;
fd = open("sample.txt", O_RDWR);
if(fd < 0) {
fprintf(stderr, "open(2) failed\n");
exit(1);
}
pid = fork();
if(pid < 0) {
fprintf(stderr, "fork(2) failed\n");
exit(1);
}
if(pid == 0) {
close(STDOUT_FILENO);
dup2(fd, STDOUT_FILENO);
char *argv[3] = {"hoge", "sample.js", NULL};
execve("/usr/bin/node", argv, NULL);
printf("execveが成功しているのであれば、このコードは実行されない\n");
exit(1);
} else {
int status;
wait(&status);
}
exit(0);
}
この状態で実行すると、sample.txt
には文字列hello
が書き込まれることが確認できた。
これによって、子プロセスでファイルに書き込んだ文字列を親プロセスでオープンしで読むことができそうな気がする。
とはいえ、ファイルのオープン・クローズだったりはオーバーヘッドが高いと聞くので普通はこんなやり方をせずに、プロセス同士でやり取りできるパイプを使う。
パイプ
パイプを使うと、ファイルを用意することなく、プロセス間でデータのやりとりが行える。
int main() {
pid_t pid;
char *argv[3] = {"node", "sample.js", NULL};
int fds[2];
char buf[100];
pipe(fds);
pid = fork();
if(pid < 0) {
fprintf(stderr, "fork(2) failed\n");
exit(1);
}
if(pid == 0) {
close(fds[0]);
close(STDOUT_FILENO);
dup2(fds[1], STDOUT_FILENO);
execve("/usr/bin/node", argv, NULL);
printf("execveが成功しているのであれば、このコードは実行されない\n");
exit(1);
} else {
int status;
close(fds[1]);
wait(&status);
read(fds[0], buf, 100);
printf("parent fds[1]: %s\n", buf);
}
return 0;
}
実際のApacheのコードを読んでも、dupを使ってることが確認できた。
FastCGI
FastCGIは既存のCGIがリクエストの度にプロセスを生成するオーバーヘッドを解消するための規格。
プロトコルの詳細はこのへんに書いてある。
https://fastcgi-archives.github.io/FastCGI_A_High-Performance_Web_Server_Interface_FastCGI.html
ApacheでFastCGIをやる場合、mod_fcgidとmod_fastcgiがある。
前者はApacheが提供していて、後者はFastCGIの規格を作った?会社が提供している。
後者は公式でメンテされることがないので、もし使うのであれば前者がよいかも。
mod_fcgidを使ってみたところ、以下の3つのプロセスが起動することが確認できる。
・Apache本体
・CGI処理用デーモン
・CGI本体
大事なのは、FastCGIの規格は、CGI処理用デーモンだけじゃなくって、CGI本体にも適用させる必要がある。
C言語でサンプルのFastCGIをつくるのであれば、以下のtiny-fcgi.cが参考になる。
https://fastcgi-archives.github.io/FastCGI_Developers_Kit_FastCGI.html
実装を見ると、CGI側でLoopしてリクエストを待ち続けていることがわかる。
Perl・Cとかのライブラリは提供されているけど、仮にNode.jsでFastCGIをやりたいとなると、自分でFastCGIの規格を読んで実装する必要がある。
※それっぽいパッケージはいくつかありそうだった。
とはいえ、Node.jsはhttpモジュールでWebサーバーとしての機能がデフォルトで用意されていて、かつノンブロッキングI/O(あんまわかってない)の考え方のもと作られているから、わざわざApache + FastCGIでNode.jsみたいな構成でやる意味はない。
https://groups.google.com/forum/#!topic/nodejs_jp/-BytMnNPshM