2007年7月頃に購入していて、ず~っと本棚に眠っていたのを、ようやく読み終えることが出来ました。
amazon.comの方でレビューを見てみると、全て高評価レビューとなっています(星4~5つ)。
以下、自分自身が読んだ感想を、三行で乱暴にまとめてみます。
まず一点目、実務でUNIXプログラミングをする上で突き当たる問題について、随所で言及・考察が加えられています。以下の三つが特に重点的に取り上げられています。
いずれも実際のUNIXプログラミングでは避けて通れない問題です。本書では、かなりしつこくこれらの問題について突っ込んでいます。突っ込みすぎて読者によっては置いてけぼりになるかもしれませんが、経験者であればあるほど、「あー、この問題かー、自分もやったなー。」と相槌を打って楽しめることでしょう。
二点目。移植性や標準についてですが、非常に注意して書かれています。そもそも本書のサンプルコード自体が、著者がSolaris/SUSE Linux/FreeBSD/Darwinの4プラットフォームで動作確認しており、どのプラットフォームがどの規格をサポートしているのか丁寧に書かれています。ただ、サポート状況の説明が各システムコール毎の解説に分散してしまっているため、一覧性は良くないかもしれません。文章を隅々まで読み込む必要があります。
逆に、実務上特定のプラットフォームしか触らないよ、という人には本書の記述は重過ぎるかもしれません。
しかし、特定のプラットフォームしか触らないにせよ、移植性を考慮するにせよ、本書の重要性は変わりません。
SUSv3では1,108の関数が規定されていますが、本書ではその中から、UNIXシステムプログラミングに関連しないCライブラリ関数・古くて利用が推奨されていないもの・logやトランザクションなどUNIXシステムプログラミングに必須とはいえないものなどを除去した、厳選に厳選を重ねた307の関数をとりあげています。
つまり本書で取り上げられている関数は、「これからの」UNIXシステムプログラミングで安心して・安定して使える関数だということです。実際、古いsignal()やBSDスタイルのflock()などはバッサリと切り捨てられ、代わりにsigaction()やlockf()/fcntl()によるロックが解説されています。
また、パス名やファイル記述子、IPCリソースなどの各種「上限値」の取得方法についても随所でsysconf()やpathconf()を使って紹介されているので、そうした情報を調べたいときにも役立ちます。
最後に読み方を工夫する必要がある点です。というのは、文章の内容が濃く、サンプルコードも初心者向きのレベルではないからです。
個人的な意見ですが、本書は「サンプルコードを自分で打ち込んで、動かして、実験して学ぶ本」ではありません。
「実務上、あるカテゴリのシステムコールを特に詳しく調査する必要が生じたときに、参考資料としてアイデアを頂戴する本」だと思います。
よって読者レベルにかかわらず、生真面目に文章の隅々まで、そしてサンプルコードを自分で打ち込んで読破する読み方はおすすめできません。たぶん、途中で精根尽き果ててしまうのではないでしょうか。
他の入門書を一冊読み終えた位のビギナーであれば、サンプルコードはもとより解説の大半も読み飛ばして結構だと思います。システムコールの網羅性は高いので、UNIXシステムプログラミングの世界地図として俯瞰して眺める程度でも罰は当たらないでしょう。
実務で実際にUNIXシステムプログラミングを経験したレベルの人であれば、ざっくりと斜め読みで通読程度でも問題ないでしょう。お仕事で必要に迫られて調査するときに、「あ~、確かこのカテゴリのシステムコール or 同期とか再突入問題とかマルチスレッド+シグナル+fork-execの最凶タッグ問題、AUPのどこそこに書いてあったな~」と思い出して、その時に詳しい解説やサンプルコードまで読み込む、というスタイルで良いと思います。
最後に、自分の読書時のメモを残しておきます。
本書のサポートサイトは以下のURLになります。
サンプルコードのダウンロード:
サンプルコードはそのままではコンパイルできません。サイト上で公開されているawkスクリプトを別途DLし、環境変数を整えた上でMakefileを生成する必要があります。詳しくは以下のURLを参照してください。
awkスクリプトを別途ダウンロードする、というのを知らずに、小一時間ほど「なんでMakefileが間違ってるんだろう~?」と悩みました。本書があくまでも"Advanced"であり、ビギナーなど眼中に無いことを思い知らされました。
自分はCentOS 5.xを使いましたが、以下のsetenv.shを用意して ". setenv.sh" すればOKにしています。
setenv.sh
#!/bin/sh AUPSRC=/home/msakamoto/in_vitro/lang.c/AUP OS=LINUX LIBS="-lncurses -lutil -lrt" TLIBS="-pthread" export AUPSRC OS LIBS TLIBS
awkスクリプトでMakefileを再生成するために、以下の makeall.sh というスクリプト使いました。
#!/bin/sh -x . setenv.sh for i in common c1 c2 c3 c4 c5 c6 c7 c8 c9 do awk -f $AUPSRC/makebuild.awk $AUPSRC/$i/makebuild.spec >$AUPSRC/$i/Makefile pushd $i #make clean make if [ 0 -ne $? ] ; then popd exit 1 fi popd done
読んでいて「へ~~へ~~、メモっとこう」と思った箇所をいくつか書き留めておきます。もっとも、Chapter5あたりからその気力すら失われました。マジで、隅々まで読み込むのはエネルギー吸い取られます。
→O_RDONLYは0で実装されてるから。・・・うわぁ・・・。
ex/o_rdwr.c:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main(void) { int fd = -1; char buf[100]; int nread = 0, nwrite = 0; char mes[] = "abc"; printf("O_RDONLY = 0x%08X\n", O_RDONLY); printf("O_WRONLY = 0x%08X\n", O_WRONLY); printf("O_RDWR = 0x%08X\n", O_RDWR); /* TEST for O_RDONLY | O_WRONLY */ if (-1 == (fd = open("test.dat", O_RDONLY | O_WRONLY))) { perror("test.dat"); return 1; } if (-1 == (nwrite = write(fd, mes, 3))) { perror("write(O_RDONLY | O_WRONLY)"); } else { printf("written size : %d\n", nwrite); } if (-1 == lseek(fd, 0, SEEK_SET)) { perror("lseek(0, (O_RDONLY | O_WRONLY))"); return 2; } if (-1 == (nread = read(fd, buf, 100))) { perror("read(O_RDONLY | O_WRONLY)"); } else { printf("read size : %d\n", nread); printf("contents = [%s]\n", buf); } close(fd); /* TEST for O_RDWR */ if (-1 == (fd = open("test.dat", O_RDWR))) { perror("test.dat"); return 1; } if (-1 == (nwrite = write(fd, mes, 3))) { perror("write(O_RDWR)"); } else { printf("written size : %d\n", nwrite); } if (-1 == lseek(fd, 0, SEEK_SET)) { perror("lseek(0, (O_RDWR))"); return 2; } if (-1 == (nread = read(fd, buf, 100))) { perror("read(O_RDWR)"); } else { printf("read size : %d\n", nread); printf("contents = [%s]\n", buf); } close(fd); return 0; }
$ gcc -o o_rdwr o_rdwr.c $ touch test.dat $ ./o_rdwr O_RDONLY = 0x00000000 O_WRONLY = 0x00000001 O_RDWR = 0x00000002 written size : 3 read(O_RDONLY | O_WRONLY): Bad file descriptor written size : 3 read size : 3 contents = [abc]
・・・これはやられた・・・
「デーモン君のソース探検」Chapter 15, mktemp も参照。
ん?open(..., | O_APPEND)直後の lseek(fd, 0, SEEK_CUR)が0を返す?write()やその後のlseek()は合ってるのに?
→同様の人、居た。
これ、ちゃんとAPUEとか読むとwrite(2)のほうに載ってるんだけど・・・
If the O_APPEND option was specified when the file was opened, the file's offset is set to the current end of file before each write operation.
つまり、open()の直後にオフセットが変更されるのではなく、write(2)の「直前に」自動的に変更されると言っている!!
OpenGroupのopen(2)ですらも、
O_APPEND If set, the file offset shall be set to the end of the file prior to each write.
つまり「毎回のwriteの直前に」としか書いていない。open(2)の直後ではないというわけだ。
O_APPENDのキモは「writeする直前に自動lseek」である。というわけで、プロセス二つ起動してwrite()させてみた。
[ttyAに切り替え] $ touch test.dat $ ./o_append2 [ttyBに切り替え] $ ./o_append2 [ttyAに切り替え] write(1) ? written size : 3 offset(1): 3 write(2) ? [ttyBに切り替え] write(1) ? written size : 3 offset(1): 6 # ちゃんとttyA側でwriteした分がseekされてる。 write(2) ? [ttyAに切り替え] written size : 3 offset(2): 9 # ちゃんとttyB側でwriteした分がseekされてる。 [ttyBに切り替え] written size : 3 offset(2): 12
O_APPENDをはずしてしまうと、writeの直前の自動lseek()が行われないため、各プロセス毎に独立してwrite(2)する。
結果として、複数のプロセスが平行してwrite(2)したとき、他のプロセスがwrite(2)したデータを上書きしてしまう。
[ttyAに切り替え] $ touch test.dat $ ./o_append3 [ttyBに切り替え] $ ./o_append3 [ttyAに切り替え] write(1) ? written size : 3 offset(1): 3 write(2) ? [ttyBに切り替え] write(1) ? written size : 3 offset(1): 3 # ttyA側を無視して、オフセット0からwriteしてる。 write(2) ? [ttyAに切り替え] written size : 3 offset(2): 6 # ttyB側を同様に無視。 [ttyBに切り替え] written size : 3 offset(2): 6
Linux/UNIXと付き合い始めてはや6年。実は、これまでhardlinkをまともに使ったことが無いという事実。いや・・・だってさ・・・シンボリックリンクでほとんど事足りちゃうんだもん・・・。ごめんね、hardlink。
で、ためしに、hardlinkしたファイルがファイルシステムをまたいでmvされちゃうとどうなるのか試してみた。
$ mkdir hardlink $ echo "12345" > hardlink/file1 $ echo "67890" > hardlink/file2 $ ln ./hardlink/file1 pfile1 $ ln ./hardlink/file2 pfile2 $ ls -il 1250716 -rw-rw-r-- 2 msakamoto msakamoto 6 11月 15 19:52 pfile1 1250717 -rw-rw-r-- 2 msakamoto msakamoto 6 11月 15 19:52 pfile2 $ cd hardlink/ ls -il 1250716 -rw-rw-r-- 2 msakamoto msakamoto 6 11月 15 19:52 file1 1250717 -rw-rw-r-- 2 msakamoto msakamoto 6 11月 15 19:52 file2 $ cd ..
この時点で、こんな感じ。
./pfile1 => hardlink/file1へのhardlink (i-node = 1250716) ./pfile2 => hardlink/file2へのhardlink (i-node = 1250717) ./hardlink/file1 (i-node = 1250716) ./hardlink/file2 (i-node = 1250717)
で、ファイルシステムをまたいでmvしてみる。ちょうど"/boot"がマウントポイントが分かれていたので、rootになって "./hardlink" ディレクトリをmvしてみる。
$ su # strace mv hardlink /boot/ ...
straceコマンドでシステムコールを追ってみると、ファイルを個別にコピーしたり、結構大変なことやってる。
hardlinkしていたpfile1, pfile2の方を見てみる。
$ ls -il 1250716 -rw-rw-r-- 1 msakamoto msakamoto 6 11月 15 19:52 pfile1 1250717 -rw-rw-r-- 1 msakamoto msakamoto 6 11月 15 19:52 pfile2
リンク数は1に減少してる。実体は存在している。結局、この場合のmvはstrace結果からも分かるように実質的には"copy"だった。
struct dirent : d_inoを参照するときは、マウントポイントか注意すること。
d_inoはディレクトリの存在するファイルシステム上のi-nodeになる。
マウントポイントの場合で、マウント先のファイルシステムのi-nodeを必要とする場合はd_inoは使えない。
d_nameメンバを使って、stat(2)なども活用してディレクトリ階層をたどっていく。
・・・これ、結構忘れそうだな・・・。「あ、わざわざd_name辿らなくても、d_inoがあるじゃん、ラッキー」ってなりそう。
while (errno = 0, (entry = readdir(dir)) != NULL)
あ、コンマのこんな使い方あったんだ。
サンプルコードの "c4/scrappc.h" でC++のbool定義に関連したコンパイルエラーが発生したので、原因と対処方法をメモ。
まず原因。
#include "defs.h"
してから、
#include <curses.h>
してる。で、一応bool定義の対処もされてるんだけど、CentOS5の場合はそれでは不十分。
curses.hの中ではC++と互換性を持つboolを定義するstdbool.hを呼んでるんだけど、defs.hの中でもstdbool.hをincludeしてる。
で、bool定義の対処が問題で、
#define bool bool_aup #include "defs.h" #undef bool #include <curses.h>
という順番になってる。
そして、stdbool.hでは二重includeを防ぐように_STDBOOL_Hマクロで囲まれてる。
結果として、
defs.h -> stdbool.h #undef bool
この時点でstdbool.hはincludeされてるが、bool定義だけ欠落した状態。
そして
#include <curses.h>
では、
#include <stdbool.h>
したあと、curses用の独自のNCURSES_BOOLをboolにdefineしてる。
#define NCURSES_BOOL bool
もしstdbool.hがまだincludeされていなかったのであれば、この時点で正常にbool定義がされるわけだが、今回のケースではずっと上のほうですでにinclude済みで、しかもbool定義はundefされている。結果として、bool定義自体はロストしたまま処理が進んでしまう。
対処について。
ちょっと無理やりだけどstdbool.hをもう一度includeさせる。
#include <curses.h>
の上に
#undef _STDBOOL_H
を置けば、二重includeチェック用のマクロをクリアできるので、defineなどのプリプロセッサも再処理される。
一応defs.hの中では、stdbool.hのincludeをSKIP_BOOLマクロでON/OFFできるようにはなってるんだけど、"-DSKIP_BOOL"をつけてみると今度は"common/ec.h"の中でもbool定義を使ってるのでそこでエラーになってしまう。事実上、defs.h中でのstdbool.hは外せない。
う~ん・・・ポータビリティって難しいなあ・・・。
参考:
SIGCHLDに対してstruct sigactionのsa_flagsにSA_NOCLDWAIT を設定するのも、sa_handlerにSIG_IGNを設定するのも効果は同等。子プロセスがゾンビにならないようにする。
ただし、SIG_IGNを設定する方法はすべてのプラットフォームでサポートされているとは限らない、らしい。
SA_NOCLDWAITはX/Openのほうで明文化されているので、SIG_IGNだけではなくてSA_NOCLDWAITも併用するのがオススメらしい。
p611, "9.1.6 sigaction System Call" のSIG_IGNとSA_NOCLDWAITの解説も併せて参照したい。
288行目くらいで
fprintf(stderr, __FILE__ ":" __func__ " -- bad state\n");
という謎の"__func__"マクロがあり、これが未定義でコンパイルに失敗した。"__FUNC__"のつもりだろうか?
とりあえず display()関数の中なので、
fprintf(stderr, __FILE__ ":display() -- bad state\n");
にしてコンパイルを通した。
・・・いや・・・実務で使ったこと無いですよ・・・そんな、聞いただけで頭が爆発しそうな組み合わせ。
"Binary Hacks"執筆協力されてるこの方、はてなダイアリのTOPで「昔のPOSIX関係の記事」に挙げてますけど、もうタイトル眺めただけでお腹一杯です・・・。
そう考えると、Threadモデル導入したApache HTTPD、結構スゴイのね。
最後に。
WebもPOSIX/SUSも、「標準規格」が後から作られてる影響が大きいせいか、「標準規格」だからといって鵜呑みに出来ないんだなってつくづく思い知らされた。
コメント