Robert Love:LINUX システムプログラミング |
作成日:2021-04-15 最終更新日: |
本書は Linux でのシステムプログラミングに焦点を当てた解説書である。正誤表などは下記にある。
https://www.oreilly.co.jp/books/9784873113623/
この本はわたしにとって難しい。
私にとってシステムプログラミングは、アプリケーションプログラムとは異なり、 魔物の住む館であった。特にプロセス管理だとか、シグナルだとか、ロックだかは、私にとってさっぱりわからなかった。
C 言語は理解していたつもりだったが、それでも難しかった。今は、Go や Rust を使えば楽に書けるのだろうか。 それ以前に概念の理解で躓くだろう。
なお、私が試した例はすべて、 WSL2 の Ubuntu 20.04 の環境である。
2章はファイル I/O について述べられている。select() や poll() などは使ったことがない。 使わずにすんでよかったのか、悪かったのかはわからない。
3章は I/O のバッファリングについて述べられている。私はこの章で取り上げられた関数は珍しくほとんど使っている。 fflush と fsync も使った覚えがある。ただ、flockfile など、ロックを明示して扱う関数は使ったことがない。
4章は「高度なファイル I/O」である。 p.90 の writev() の動作を示すサンプルコードをコンパイルしようとしたらエラーが出た:
$ make cc -Wall -Wextra -O2 -lrt -g writev.c -o writev In file included from /usr/include/fcntl.h:301, from writev.c:5: In function ‘open’, inlined from ‘main’ at writev.c:20:10: /usr/include/x86_64-linux-gnu/bits/fcntl2.h:50:4: error: call to ‘__open_missing_mode’ declared with attribute error: → open with O_CREAT or O_TMPFILE in second argument needs 3 arguments 50 | __open_missing_mode (); | ^~~~~~~~~~~~~~~~~~~~~~ make: *** [<ビルトイン>: writev] エラー 1
調べてみると、おそらくソースファイルの 20 行め
fd = open("buccaneer.txt", O_WRONLY | O_CREAT | O_TRUNC);
この open の引数が2つしかないことが原因ではないだろうかと推察できる。本書の p.27 ではこう書かれている。
open() システムコールのパラメータ数には2通りがあることは前掲の通りですが、誤りではありません。 mode はファイルを作成する場合以外では無視され、O_CREAT フラグを使用した場合にのみ必要になります。 O_CREAT フラグを指定したけれど、mode を渡し忘れた場合の結果は未定義で、好ましくない状態になるでしょう。 くれぐれも mode を忘れないように。
本書でこの念押しに反しているのだから、つい笑ってしまった。仕方がないので、
パーミッションとして 0644 を指定した。なお、警告が出ていたの個所があった。
printf("wrote %d bytes\n", nr);
ここの nr は int ではなく、ssize_t だから、%d ではなく %ld を使うように、
というのがコンパイラのご託宣だった。
これを直して、 writev のサンプルコードは動いた。
ところが readv のサンプルコードが動かない。まず、次の行
printf("%d %.*s", i, iov[i].iov_len, (char *)iov[i].iov_base);
に誤りがあるように見えた。変数が3つあるのに表示書式では2種類しかないじゃないか。
しかし、printf の書式としては正しいのである。 %.*s という表示は、最初の引数で与えられた数の分だけ、
文字を表示するという意味だ。ただ、この文のままでは警告が出る:
$ make cc -Wall -Wextra -O2 -lrt -g readv.c -o readv readv.c: In function ‘main’: readv.c:37:22: warning: field precision specifier ‘.*’ expects argument of type ‘int’, but argument 3 has type ‘size_t’ {aka ‘long unsigned int’} [-Wformat=] 37 | printf("%d %.*s", i, iov[i].iov_len, (char *)iov[i].iov_base); | ~~^~ ~~~~~~~~~~~~~~ | | | | int size_t {aka long unsigned int}
そこで、この行のiov[i].iov_len を、(int)iov[i].iov_len とキャストした。仕方がない。
printf("%d %.*s", i, (int)iov[i].iov_len, (char *)iov[i].iov_base);
私はここを誤って次のように<直した>つもりでいたところ、これでは逆にバグが出てしまうことになった。:
printf("%d %ld %s", i, iov[i].iov_len, (char *)iov[i].iov_base);
(char *)iov[i].iov_base が NULL 終端されていないので、 不可解な文字列が表示されてしまうというバグである。
5章は「プロセス管理」である。pp.165-166 をまねてデーモンプログラムを作ってみたが、 コンパイルできない。
$ make cc -Wall -Wextra -O2 -lrt -g daemon.c -o daemon daemon.c: In function ‘main’: daemon.c:34:20: error: ‘NR_OPEN’ undeclared (first use in this function) 34 | for (i = 0; i < NR_OPEN; i++) { | ^~~~~~~ daemon.c:34:20: note: each undeclared identifier is reported only once for each function it appears in daemon.c:41:4: warning: ignoring return value of ‘dup’, declared with attribute warn_unused_result [-Wunused-result] 41 | dup(0); /* stderr */ | ^~~~~~ make: *** [<ビルトイン>: daemon] エラー 1
後者は警告であり、dup(0) に関しては戻り値を調べるように作り変えたることができた。 しかし、前者の NR_OPEN が宣言されていないエラーはわからない。たぶん、もう、使われていない値なのだろう。
6章は「高度なプロセス管理」である。5章は単なる「プロセス管理」であったから、 さらに私にとってしきいが高くなっている。おまけにこの章は、main で始まる C プログラムが一つもない。 コードの一部だけで高度なプロセス管理を理解する気は(しゃれではないが)起こらない。 それでも、一つぐらいは何か試したい。リソースリミットの参照/設定を試してみた。p.198 と p.199 にある。
7章はファイル、ディレクトリの管理という表題がある。 特定のディレクトリ配下のサブディレクトリとファイルをすべて表示する、 というディレクトリとファイルの渡り歩きのような例はなかった。自分で考えろということなのだろう。
7.7 節は inotify である。main 関数はないが、コードの断片があるので自分で main 関数を考えながら作ってみることにした。まず p.244 の初期化からである。
/* inotify.c */ #include <sys/inotify.h> #include <stdlib.h> #include <stdio.h> int main(void) { int fd; fd = inotify_init(); if (fd == -1) { perror("inotify_init"); exit(EXIT_FAILURE); } }
p.244 では #include <inotify.h> となっているが、これでは次のように言われてしまった。
inotify.c:2:10: fatal error: inotify.h: そのようなファイルやディレクトリはありません
次に 7.7.3 inotify イベントのコードがコンパイルできるか試してみた。
7.7.3.1 では BUF_LEN が具体的にいくつなのかがわからない。
http://manpages.ubuntu.com/manpages/focal/ja/man7/inotify.7.html
を見て 4096 でいいだろうと思い、次の宣言を加えた。
const int BUF_LEN = 4096;
また、 read() を使うので対応するヘッダファイル
#include <unistd.ht>
を加えた。
ここまで実装して、果たしてうまくいっているのかどうか不安になった。
$ ./inotify & [1] 1883 $ ls /etc wd=1 mask=1073741825 cookie=0 len=0 dir=yes (/etc 配下のディレクトリとファイルが表示される) [1]+ 終了 ./inotify
うまくいったようだ。
8章はメモリ管理について書かれている。p.261 ではメモリリークや解放後の不正なアクセスについての説明があった。 あるシステムについて、メモリについての悩みを当事者として抱えていたころ、 出会ったのが Purify であった。今では古いソフトであり、もう誰も使っていないと思うが、 当時はわらにもすがる思いで使ったのだった。ちなみに、今は、 本書にあるような Electrice Fence や Valgrind になるのだろうか。特に、 後者の Valgrind はよく聞く名前だ。
9章はシグナルについて解説されている。10章から先に読み進めたが、シグナルの理解が前提だったので、 やはり本は最後から読んではいけないのだとがっかりした。
p.297 でシグナルのハンドラーの関数がある。signal 関数に登録するハンドラー関数は、 シグナル番号を引数にとらなければならないが、引数を使わないこともある。しかし、 使わない引数をそのまま使わないと、コンパイラは警告を出す。
timer.c:10:24: warning: unused parameter ‘signo’ [-Wunused-parameter] 10 | void alarm_handler(int signo) { | ~~~~^~~~~
この警告を消すためにはどうすべきか。コンパイル時のオプションを工夫して出さなくすることはできるが、
今度は本当に不要な変数についても警告が出なくなるので困る。stack overflow などを見て、
(void)signo;
とする方法があることがわかった。気が利かないが、これでよしとしたい。
9 章の前半は旧来の signal() や kill() が説明され、
より新しい sigaction() や sigqueue() を使うインターフェースは高度なシグナル処理として記述されている。
本書でも p.316 でシグナルのもう1つの弱点は、多くのプログラマがシグナス捜査に sigaction()、sigqueue()
へ移行せず、いまだに signal() および kill() を使用している点です。
と述べられている。sigaction() は高度なシグナル処理というより、
こちらが基本のシグナル処理として考えていくべきだろう。本書が刊行された2010 年前ならまだしも、
今は 2021 年なのだから。ただ、本書は、sigaction() や sigqueue() について、
API の説明はあるがサンプルコードの記載がほとんどない。このようなところが、sigaction()
が普及していない点の原因なのかもしれない。
システムプログラム(2021年度第5回)
(www.coins.tsukuba.ac.jp) では、シグナルの基礎知識がある。ここでも、
POSIX 準拠のシステムであれば sigaction を使用すべきであり,signal はもはや使用すべきではない.
という文がある。
ということで、本文 p.297 のサンプルコードを sigaction を使って書き直した。
/* sigaction.c */
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
/* handelr for SIGINT */
static void sigint_action(int signo, siginfo_t *info, void *ctx) {
(void)signo;
(void)info;
(void)ctx;
printf("Caught SIGINT!\n");
exit(EXIT_SUCCESS);
}
int main(void) {
struct sigaction sa_sigint;
memset(&sa_sigint, 0, sizeof(sa_sigint));
sa_sigint.sa_sigaction = sigint_action;
sa_sigint.sa_flags = SA_RESTART;
if (sigaction(SIGINT, &sa_sigint, NULL) < 0) {
fprintf(stderr, "Cannot handle SIGINT!\n");
exit(EXIT_FAILURE);
}
for (;;) {
pause();
}
return 0;
}
10章は時間についての話題である。私にとってこの本は敷居が高いと思ったので、 敷居が低そうな、時間に関する話題の章から読み始めたのだが、やはり敷居は高かった。
pp.323-324 では、時計の分解能について解説されている。本書では p.323 の結果が次のようになっている( clock=[0-3] の意味に関しては省略する。
clock=0 sec=0 nsec=4000250 clock=1 sec=0 nsec=4000250 clock=2 sec=0 nsec=1 clock=3 sec=0 nsec=1
私の環境ではこうなった。
clock=0 sec=0 nsec=1 clock=1 sec=0 nsec=1 clock=2 sec=0 nsec=1 clock=3 sec=0 nsec=1
ということは、今の LINUX では、4種類の時計すべてで、ナノ秒の分解能をもっていることがわかる。 なお、p.324 にある TSC とは Time Stamp Counter という、CPU クロックを発信器とする高分解能の時計を意味し、 Pentium 以降で利用可能な時計である。
p.336 では、スリープの関数について、sleep() や usleep() と比較して nanosleep() の利点を述べている。その理由を引用する。
最後の理由にひっかかった。この落とし穴とは何だろう。これは、sleep() や usleep() はシグナルを用いている (かもしれない)ので、それがもとで何か問題が起こる、と読むべきだろう。その後述というのは p.324 にあった。 時間に関するシステムプログラミングで sleep とともに用いられるのはタイマである。 このタイマのうち、alarm() と setitimer() は SIGALAM を使用する。また、sleep() や usleep() でも SIGALAM を使用する Unix システムがあるという。ということは、どちらもシグナルを使う可能性がある、 ということだ。タイマ系とスリープ系で SIGALRM が重複する場合の場合は未定義だという。この p.324 では、 タイマ系では nanosleep を使うべきだといっている。
p.83 「3.14.1 ファイルの手動ロック」のすぐ下、「ロックが解放されるの待ってから、」 → 「ロックが解放されるのを待ってから、」
p.90 脚注、「標準でもこの動作の許容しており、」 → 「標準でもこの動作を許容しており、」
p.316 中ほど「(不正はオペコード実行)」とあるが、おそらく「(不正なオペコード実行など)」の誤りだろう。
p.324 第1段落最後「肝に命じてください」とあるが、「肝に銘じてください」が正しい。
書 名 | LINUX システムプログラミング |
著 者 | Robert Love |
訳 者 | 千住 治郎 |
発行日 | 2008 年 4 月 14 日(初版第1刷) |
発行元 | オライリー・ジャパン |
定 価 | 円(本体) |
サイズ | |
ISBN | 978-4-87311-362-3 |
その他 | 越谷市立図書館で借りて読む |
まりんきょ学問所 > コンピュータの部屋 > Unix, Linux > Robert Love:LINUX システムプログラミング