「ソースコードを見ないことには始まらない」Seventh Editionは、結果的に当時のユーザーの急速なスキ ルアップに大きく貢献しました。今回は当時のユーザーがどのようにしてソースコードに取り組んだか再現 してみようと思います。皆さんのスキルアップにもきっと有効でしょう。
時折「現在、最も優れたプログラミング言語って何だ ろうな?」と考えることがあります。最近のUnixにはた いていC以外にC++やPerl、Java、PythonにRubyなどな ど、さまざまなプログラミング言語がインストールされ ていて、正直「メジャーなプログラミング言語がこんな にたくさんあるんじゃたまらない」と思ったりします。い ままさにプログラミングを勉強している、あるいはこれ からプログラミングを勉強しようとしている若い方々も、 同じ戸惑いを感じているのではないでしょうか?
Unixが登場した当時の、コンピュータユーザーの置か れた状況もこれとよく似ていたように思います。これま でにご紹介してきたように、当時はメーカーごとに異な るシステムが乱立していた時代でして、A社のコンピュ ータでプログラムを書くときにはこれ、B社のコンピュ ータのためのプログラムを書くにはあれとか。揚げ句の 果てに、同じメーカーのコンピュータでも、シリーズご とに開発システムはまったく違っていて相互に互換性が ないなんてことも間々ありました。各種のプログラミン グ言語や開発システムが乱立している状況はいまと変わ らないような気がします。
こういう状況の中でUnixやCに人気が集まったのは、 やっぱりシンプルだったからでしょう。とにかく、覚え なければならないことが大変少なかったのです。プログ ラミング言語もいわゆる一般的な言語と同じで、最初の うちは丸暗記するしかありません。ですから、覚えなけ ればならないことが少なければ少ないほど、習得が早い というわけです。そして、UnixはCだけで何でもできる システムでした。覚えたばかりのわずかな知識だけでも 結構いろいろできますし、さらにソースコードを読んで 自分だけで新しい知識を習得することもできます。当時 の大学生がUnixを持てはやしたのには、自分のペースで 習得していけるシステムだったことに負うところが大き いでしょう。
今回は読者の皆さんにもそれを疑似体験してもらおう と思います。
実行例1 カーネルの再構築
#
# cd /usr/sys/h → /usr/sys/h に移動
# cp param.h param.h.orig →元のファイルをコピー
# ed param.h → ed コマンドを使って2つのマクロを変更
4411
/TIMEZONE/p →マクロ TIMEZONE を検索
#define TIMEZONE (5*60) /* Minutes westward from Greenwich */
s/5/-9/p → 5 を -9 に置き換え
#define TIMEZONE (-9*60) /* Minutes westward from Greenwich */
/DSTFLAG/p →マクロ DSTFLAG を検索
#define DSTFLAG 1 /* Daylight Saving Time applies in this locality */
s/1/0/p → 1 を 0 に置き換え
#define DSTFLAG 0 /* Daylight Saving Time applies in this locality */
w →変更内容をファイルにセーブ
4412
q → ed コマンドを終了
# diff param.h.orig param.h
22,23c22,23
< #define TIMEZONE (5*60) /* Minutes westward from Greenwich */
< #define DSTFLAG 1 /* Daylight Saving Time applies in this locality */
---
> #define TIMEZONE (-9*60) /* Minutes westward from Greenwich */
> #define DSTFLAG 0 /* Daylight Saving Time applies in this locality */
#
#
# mv /usr/include/sys/param.h /usr/include/sys/param.h.orig
# cp param.h /usr/include/sys/
#
#
# cd /usr/sys/conf
# rm ../sys/LIB1 ../dev/LIB2
# rm *.o
# make all
cd ../sys; cc -c -O *.c; mklib; rm *.o
acct.c:
alloc.c:
clock.c:
....
text.c:
trap.c:
ureg.c:
cd ../dev; cc -c -O *.c; mklib; rm *.o
bio.c:
cat.c:
dc.c:
....
tty.c:
vp.c:
vs.c:
# make
かつて私が Unix ワークステーションを使い始めた頃、インストール直後に カーネルを再構築をしなければならない理由といえば、タイムゾーン設定の 変更でした。
当時はようやく LAN が一般のオフィスに普及し始めたばかりの時期で、 インターネットとの接続は夢のまた夢でしたので、タイムゾーンの設定など 気にしなければまったく気にならないようなものでした。(事実、タイムゾーンが EST のままで利用されているワークステーションはオフィスにゴロゴロしていました) にもかかわらず、当時の多くの Unix オタクがタイムゾーンの設定に こだわったのには理由があります。当時最新の Unix だった Sun OS (Solaris ではない)などでは起動時にカーネルがタイトルメッセージを出しますが、 ★SolarisもSun OSなので、括弧内はカット。もしくは注できちんと説明する そこにはカーネルを再構築した時間がタイムゾーン付きで表示されます。 で、うっかりそのタイムゾーンが EST と表示するような状態になっていたとしたら、 それは「私は Unix のシロウトです」といっているようなものでした。というわけで、 単にバカにされないためだけに、OS のインストール直後にカーネルを再構築を 2回繰り返したものでした。そして他のワークステーションで起動時に EST を 目にしたら、そのマシンの管理者にはあからさまに小馬鹿にした態度を 取っていました。まったく無礼な話です。
BTL の所在地はニュージャージ州(ニューヨーク州のとなりですね)ですから、 当然のことながら Seventh Edition のデフォルトタイムゾーンがアメリカ 東海岸時間(EST)に設定されています。もちろん夏時間の期間にはタイムゾーンが 自動的に切り替わる仕掛けもデフォルトで ON になっています。 日本国内で使用する場合はいずれも変更するべきでしょう。 セットアップドキュメントにも解説があるように、Seventh Edition で タイムゾーンの設定を変更するためには、カーネルが使用するシステムヘッダー ファイル /usr/sys/h/param.h を修正して、カーネルの再構築しなければ なりません。ではタイムゾーンの設定を変更する説明をしましょう。 ★実行例1をご覧ください。
ここでは TIMEZONE と DSTFLAG という2つのマクロを修正しています。 TIMEZONE には設定したいタイムゾーンのグリニッジ標準時からの時差を 分単位で設定しますが、日本標準時の場合は9時間早いので (-9*60) となります。 DSTFLAG は夏時間切替のためのフラグですが、日本では夏時間は 使われませんので 0 を設定します。修正には ed コマンドを使いますが、 使い慣れていない方は実行例の中のコメントを参考にしてください。
修正が終ったら、例によって diff で修正内容を確認してください。 さらにカーネルが使用するヘッダーファイル (/usr/sys/h にある ヘッダーファイル) は、システムヘッダーファイルとして /usr/include/sys にも コピーが置かれています。修正した param.h を /usr/include/sys にもコピー しておいてください。
以上でタイムゾーンに関連する修正は終わりです。 /usr/sys/conf に移動してカーネルの再構築を行ってください。 詳細は前回の記事を参考にしてください。
実行例2 ライブラリの修正と再コンパイル
$ pdp11
PDP-11 simulator V3.0-2
sim> set cpu 18b
sim> set rp0 RP06
sim> att rp0 CH8.DSK
sim> boot rp0
boot
Boot
: hp(0,0)unix
mem = 175552
# <CNTL-D>
RESTRICTED RIGHTS: USE, DUPLICATION, OR DISCLOSURE
IS SUBJECT TO RESTRICTIONS STATED IN YOUR CONTRACT WITH
WESTERN ELECTRIC COMPANY, INC.
THU JAN 1 10:43:34 GMT+9:00 1970
login: root
Password:
You have mail.
#
#
# cd /usr/src/libc/gen → /usr/src/libc/gen に移動
# cat timezone.c
/*
* The arguments are the number of minutes of time
* you are westward from Greenwich and whether DST is in effect.
* It returns a string
* giving the name of the local timezone.
*
* Sorry, I don't know all the names.
*/
static struct zone {
int offset;
char *stdzone;
char *dlzone;
} zonetab[] = {
4*60, "AST", "ADT", /* Atlantic */
5*60, "EST", "EDT", /* Eastern */
6*60, "CST", "CDT", /* Central */
7*60, "MST", "MDT", /* Mountain */
8*60, "PST", "PDT", /* Pacific */
0, "GMT", 0, /* Greenwich */
-1
};
char *timezone(zone, dst)
{
register struct zone *zp;
static char czone[10];
char *sign;
for (zp=zonetab; zp->offset!=-1; zp++)
if (zp->offset==zone) {
if (dst && zp->dlzone)
return(zp->dlzone);
if (!dst && zp->stdzone)
return(zp->stdzone);
}
if (zone<0) {
zone = -zone;
sign = "+";
} else
sign = "-";
sprintf(czone, "GMT%s%d:%02d", sign, zone/60, zone%60);
return(czone);
}
# cp timezone.c timezone.c.orig →元のファイルを退避
# ed timezone.c → ed コマンドで修正
941
/AST/p → AST を検索
4*60, "AST", "ADT", /* Atlantic */
i →行を挿入
-9*60, "JST", 0, /* Japan */
. →挿入終了
w →修正内容をセーブ
972
q → ed コマンドを終了
# diff timezone.c.orig timezone.c
14a15
> -9*60, "JST", 0, /* Japan */
#
#
# cd /usr/src/libc → /usr/src/libc に移動
# sh -x compall → ライブラリソースのコンパイル
+ cc -c -O /usr/src/libc/stdio/getgrgid.c
+ cc -c -O /usr/src/libc/stdio/getgrnam.c
+ cc -c -O /usr/src/libc/stdio/getgrent.c
....
+ cc -c -O /usr/src/libc/gen/getlogin.c
+ cc -c -O /usr/src/libc/gen/perror.c
+ cc -c -O /usr/src/libc/gen/sleep.c
+ cc -c -O /usr/src/libc/gen/timezone.c
+ cc -c -O /usr/src/libc/gen/ttyslot.c
+ cc -c -O /usr/src/libc/gen/ttyname.c
+ cc -c /usr/src/libc/gen/abort.s
....
+ cc -c /usr/src/libc/crt/lrem.s
+ cc -c /usr/src/libc/crt/mcount.s
+ cc -c /usr/src/libc/crt/csv.s
# sh -x mklib →ライブラリの作成
+ ar rc libc.a getgrgid.o getgrnam.o getgrent.o getpass.o getpwnam.o getpwuid.o getpwent.o timezone.o fgetc.o fputc.o getchar.o putchar.o popen.o freopen.o fgets.o fputs.o getpw.o fseek.o ftell.o rew.o rdwr.o system.o fopen.o fdopen.o scanf.o doscan.o fprintf.o gets.o getw.o printf.o puts.o putw.o sprintf.o ungetc.o filbuf.o setbuf.o fltpr.o doprnt.o gcvt.o ffltpr.o strout.o flsbuf.o endopen.o findiop.o clrerr.o data.o cuexit.o execvp.o getenv.o getlogin.o perror.o sleep.o ttyslot.o ttyname.o abort.o abs.o atof.o atoi.o atol.o crypt.o ctime.o calloc.o malloc.o ecvt.o errlst.o fakcu.o fakfp.o frexp11.o isatty.o l3.o ldexp11.o ldfps.o mktemp.o modf11.o mon.o mpx.o nlist.o qsort.o rand.o setjmp.o stty.o swab.o tell.o ctype_.o index.o rindex.o strcat.o strncat.o strcmp.o strncmp.o strcpy.o strncpy.o strlen.o access.o acct.o alarm.o chdir.o chroot.o chmod.o chown.o close.o creat.o dup.o execl.o execle.o execv.o execve.o exit.o fork.o fstat.o getgid.o getpid.o getuid.o ioctl.o kil!
l.o link.o lock.o lseek.o mknod.o
mount.o mpxcall.o nice.o open.o pause.o phys.o pipe.o profil.o ptrace.o read.o sbrk.o setgid.o setuid.o signal.o stat.o stime.o sync.o time.o times.o umask.o umount.o unlink.o utime.o wait.o write.o aldiv.o almul.o alrem.o cerror.o ldiv.o lmul.o lrem.o mcount.o csv.o
# mv /lib/libc.a /lib/libc.a.orig →元の libc を退避
# mv libc.a /lib/ →新しい libc を /lib に移動
#
#
# cd /usr/src/cmd → /usr/src/cmd に移動
# cmake date → date コマンドをコンパイル
date:
# ./date →その場で実行
Thu Jan 1 10:54:56 JST 1970
#
#
# mv /bin/date /bin/date.orig →元の date コマンドを退避
# cp date /bin/date →新しい date コマンドをコピー
#
#
# kill 1
# sync
# sync
# sync
# sync
#
Simulation stopped, PC: 016572 (JSR PC,2460)
sim> quit
Goodbye
$
さて Seventh Edition のカーネルでは分単位での時差でタイムゾーンを表しますが、 これを EST や JST といった一般的な表記に変換する timezone という ルーチンが libc.a に存在します。date コマンドなど Seventh Edition の 時差に関連するコマンドはこのルーチンを使ってタイムゾーンを示す文字列を得ます。
セットアップドキュメントにも説明がありますが、この timezone というルーチンは 内部に変換用のテーブルを持っています。困ったことにデフォルトではグリニッジ 標準時の表記である GMT と、アメリカ国内で使用されるタイムゾーンの表記 (AST/EST/CST/MST/PST) しか定義されていません。日本標準時の表記である JST を表示させるためには、内部の変換用テーブルに新しいエントリを追加する 必要があります。ここではこのエントリを追加する手順と libc.a ライブラリを 再コンパイルする手順を説明します。★実行例2をご覧ください。
timezone ルーチンはソースファイル /usr/src/libc/gen/timezone.c に 定義されています。まず内容を確認しましょう。 zonetab という構造体の配列が変換用のテーブルです。 このテーブルの先頭に JST のためのエントリを追加します。 ここでは大西洋標準時 (AST/ADT) のエントリの前に 日本標準時 (JST) のエントリを追加しています。 offset には先ほどのマクロ TIMEZONE で指定した値 -9*60 を設定します。 stdzone には文字列 "JST" を設定します。それから、日本には夏時間は ありませんので dlzone には 0 を設定します。
修正が終わったら、シェルスクリプト /usr/src/libc/compall を 使って libc.a の再コンパイルを行います。 次に、シェルスクリプト /usr/src/libc/mklib を使って コンパイル済みのオブジェクトから libc.a を作成します。 いずれのシェルスクリプトも -x オプションを使用すると 実行中のコマンドが表示されます。これで libc.a が作成されました。 元の libc.a を退避して、新しい libc.a を /lib ディレクトリに 移動してください。以上で libc.a の更新は終わりです。
修正した timezone ルーチンが正しく動作するか確認するためには timezone ルーチンを使用しているプログラムを再コンパイルして 動かしてみることが手っ取り早い方法です。ここでは date コマンド を再コンパイルしてみましょう。ここではシェルスクリプト /usr/src/cmd/cmake を 使って、date コマンドのコンパイルを行っています。セットアップドキュメントにも 説明されているように、Seventh Edition のほとんどのコマンドは cmake を使って コンパイルすることができます。
再コンパイルが終わったら、その場で date コマンドを実行してみてください。 タイムゾーンの表記は JST に変わっているでしょうか? 表示されている時間が狂っているのは誤りではありません。 まだ正しい時間を設定していませんからね。
正しく動くことが確認できたら元の date コマンドと置き換えましょう。
実行例3 date コマンドの Y2K 問題対応
$ pdp11
PDP-11 simulator V3.0-2
sim> set cpu 18b
sim> set rp0 RP06
sim> att rp0 RP0.DSK
sim> boot rp0
boot
Boot
: hp(0,0)unix
mem = 175552
# <CNTL-D>
RESTRICTED RIGHTS: USE, DUPLICATION, OR DISCLOSURE
IS SUBJECT TO RESTRICTIONS STATED IN YOUR CONTRACT WITH
WESTERN ELECTRIC COMPANY, INC.
THU JAN 1 11:00:48 GMT+9:00 1970
login: root
Password:
You have mail.
#
#
# date 200310232055 →2000年以降の時間が設定できない
date: bad conversion
#
#
# cd /usr/src/cmd → /usr/src/cmd に移動
# awk '{ printf("%4d ", NR); print $0 }' date.c →行番号付きで表示
1 /*
2 * date : print date
3 * date YYMMDDHHMM[.SS] : set date, if allowed
4 * date -u ... : date in GMT
5 */
6 #include <time.h>
7 #include <sys/types.h>
8 #include <sys/timeb.h>
9 #include <utmp.h>
10 long timbuf;
11 char *ap, *ep, *sp;
12 int uflag;
13
14 char *timezone();
15 static int dmsize[12] =
16 {
17 31,
18 28,
19 31,
20 30,
21 31,
22 30,
23 31,
24 31,
25 30,
26 31,
27 30,
28 31
29 };
30
31 struct utmp wtmp[2] = { {"|", "", 0}, {"{", "", 0}};
32
33 char *ctime();
34 char *asctime();
35 struct tm *localtime();
36 struct tm *gmtime();
37
38 main(argc, argv)
39 char *argv[];
40 {
41 register char *tzn;
42 struct timeb info;
43 int wf, rc;
44
45 rc = 0;
46 ftime(&info);
47 if (argc>1 && argv[1][0]=='-' && argv[1][1]=='u') {
48 argc--;
49 argv++;
50 uflag++;
51 }
52 if(argc > 1) {
53 ap = argv[1];
54 if (gtime()) {
55 printf("date: bad conversion\n");
56 exit(1);
57 }
58 /* convert to GMT assuming local time */
59 if (uflag==0) {
60 timbuf += (long)info.timezone*60;
61 /* now fix up local daylight time */
62 if(localtime(&timbuf)->tm_isdst)
63 timbuf -= 60*60;
64 }
65 time(&wtmp[0].ut_time);
66 if(stime(&timbuf) < 0) {
67 rc++;
68 printf("date: no permission\n");
69 } else if ((wf = open("/usr/adm/wtmp", 1)) >= 0) {
70 time(&wtmp[1].ut_time);
71 lseek(wf, 0L, 2);
72 write(wf, (char *)wtmp, sizeof(wtmp));
73 close(wf);
74 }
75 }
76 if (rc==0)
77 time(&timbuf);
78 if(uflag) {
79 ap = asctime(gmtime(&timbuf));
80 tzn = "GMT";
81 } else {
82 struct tm *tp;
83 tp = localtime(&timbuf);
84 ap = asctime(tp);
85 tzn = timezone(info.timezone, tp->tm_isdst);
86 }
87 printf("%.20s", ap);
88 if (tzn)
89 printf("%s", tzn);
90 printf("%s", ap+19);
91 exit(rc);
92 }
93
94 gtime()
95 {
96 register int i, year, month;
97 int day, hour, mins, secs;
98 struct tm *L;
99 char x;
100
101 ep=ap;
102 while(*ep) ep++;
103 sp=ap;
104 while(sp<ep) {
105 x = *sp;
106 *sp++ = *--ep;
107 *ep = x;
108 }
109 sp=ap;
110 time(&timbuf);
111 L=localtime(&timbuf);
112 secs = gp(-1);
113 if(*sp!='.') {
114 mins=secs;
115 secs=0;
116 } else {sp++;
117 mins = gp(-1);
118 }
119 hour = gp(-1);
120 day = gp(L->tm_mday);
121 month = gp(L->tm_mon+1);
122 year = gp(L->tm_year);
123 if(*sp)
124 return(1);
125 if( month<1 || month>12 ||
126 day<1 || day>31 ||
127 mins<0 || mins>59 ||
128 secs<0 || secs>59)
129 return(1);
130 if (hour==24) {
131 hour=0; day++;
132 }
133 if (hour<0 || hour>23)
134 return(1);
135 timbuf = 0;
136 year += 1900;
137 for(i=1970; i<year; i++)
138 timbuf += dysize(i);
139 /* Leap year */
140 if (dysize(year)==366 && month >= 3)
141 timbuf++;
142 while(--month)
143 timbuf += dmsize[month-1];
144 timbuf += day-1;
145 timbuf = 24*timbuf + hour;
146 timbuf = 60*timbuf + mins;
147 timbuf = 60*timbuf + secs;
148 return(0);
149
150 }
151
152 gp(dfault)
153 {
154 register int c, d;
155
156 if(*sp==0)
157 return(dfault);
158 c = (*sp++)-'0';
159 d = (*sp ? (*sp++)-'0' : 0);
160 if(c<0 || c>9 || d<0 || d>9)
161 return(-1);
162 return(c+10*d);
163 }
#
#
# date -u 9912312359.59 →グリニッジ標準時 (GMT) で時刻設定
Fri Dec 31 23:59:59 GMT 1999
# date →日本標準時 (JST) で時刻表示
Sat Jan 1 09:00:00 JST 2000
#
#
# cp date.c date.c.orig
# ed date.c → ed コマンドで date.c を修正
2639
136c → 136 行目を修正
if (high == 0) {
year += 1900;
}
. →修正終了
122a → 122 行目の後ろに行を追加
if ((high = gp(0)) != 0) {
year += high*100;
}
. →追加終了
95a → 95 行目の後ろに行を追加
register int high;
. →追加終了
w →修正内容をセーブ
2734
q → ed コマンドを終了
#
#
# diff date.c.orig date.c →修正内容の確認
95a96
> register int high;
122a124,126
> if ((high = gp(0)) != 0) {
> year += high*100;
> }
136c140,142
< year += 1900;
---
> if (high == 0) {
> year += 1900;
> }
#
#
# cmake date → date.c のコンパイル
date:
#
#
# ./date 9912312359 →2桁の年数での時刻指定
Fri Dec 31 23:59:00 JST 1999
# ./date 200310232055 →4桁の年数での時刻指定
Thu Oct 23 20:55:00 JST 2003
#
#
# rm /bin/date → date コマンドの置き換え
# cp date /bin/
#
#
# kill 1
# sync
# sync
# sync
# sync
#
Simulation stopped, PC: 002466 (RTS PC)
sim> quit
Goodbye
$
さて Seventh Edition で時刻設定を行うには問題があります。 その問題とは 2000 年以降の時刻を設定することができない、 つまりいわゆる Y2K 問題が存在します。★実行例3を御覧ください。 2000 年以降の日付と時刻を設定しようとするとエラーが発生します。
かつて Y2K 問題で大騒ぎになった 1990 年代より 更にずっと前の 1979 年にリリースされた Seventh Edition では この問題が考慮されていないのも当然といえば当然ですが、 もはや誰もメンテナンスをしていないこのシステムのトラブルは 自分で直すしかありません。まさに当時のユーザーと同じ状況です :-)
というわけでどこを直せば良いのか調べてみましょう。 まず date コマンドのソースを表示します。 Seventh Edition の cat コマンドは -n というような便利なオプションは ありません。行番号付きでソースを表示するには awk など他のコマンドを 使う必要があります。
パッと見てもわかるように、今日の基準だとこのソースはC言語の書き方の お手本にはなりそうにないコードです。これも K&R 時代(★注1)のなごりだと 好意に理解してくださいね。
このプログラムの main() は 38 行目から始まっています。 まず注目すべきは 47 行目から始まっているオプションのチェックです。 man page には記述されていませんが、date コマンドは -u オプションを サポートしています。このオプションは皆さんも良くご存知でしょう。 時刻指定あるいは時刻表示は全てグリニッジ標準時 (GMT) を使用する オプションです。このように当時は man page には記述されていない オプションが結構存在していて、そういった隠れオプションを探すのも Unix ハッカーのお楽しみの1つでした。
main() の構造をザッと見ると、前半 (52 行目から 75 行目まで) は 引数が存在する場合、すなわち date コマンドで時刻を設定する処理が 記述してあります。後半 (76 行目から 91 行目まで) には時刻を表示する 処理が記述してありますが、これは引数のあるなしに関わらず実行されます。
それでは共通する後半部分の処理から見ていきましょう。まず 77 行目で time() システムコールを使って、システムクロックの値を変数 timbuf に 代入しています。システムクロックとはグリニッジ標準時の 1970/1/1 00:00:00 から 始まり秒単位でカウントアップするカーネル内のカウンタです。多くの Unix では このシステムカウンタの値から現在時間を計算しています。 79 行目と 80 行目は uflag が True の時、すなわち -u オプションが 指定された場合の処理としてグリニッジ標準時での日付と時刻の表示を行います。 一方、82 行目から 85 行目までは -u オプションが指定されなかった場合の 処理としてローカルなタイムゾーンでの日付と時刻の表示を行います。 いずれもライブラリルーチンの gmtime() localtime() asctime() を使用して、 表示する日付と時刻の文字列を組み立てていますから、2000/1/1 00:00:00 以降の 表示が可能かどうかはこれらのルーチンの処理内容を調べる必要があります。
そこでライブラリルーチンの処理内容を調べるために簡単な実験をしてみましょう。 ここではグリニッジ標準時(GMT)で 1999/12/31 23:59:00 の時刻を設定し、 ローカルタイム(JST)で表示をしています。 これは西暦の下2桁しか受け付けない date コマンドを使って 2000 年以降の 日付/時刻を設定するためのトリックです。イギリスの時刻が 1999/12/31 23:59:00 の時、それより時刻が9時間早い日本では 2000/1/1 08:59:00 ですから、 ライブラリルーチンが正しく動作しているのであれば 2000/1/1 の表示を するはずです。で、結果はご覧のとおり。Seventh Edition のライブラリ ルーチンは Y2K 問題をちゃんと考慮しています。(これはちょっと驚きですね)
main() の後半部分が問題なしということであれば、残る前半部分を直せば 日付と時間を正しく設定できるようになるでしょう。54 行目を見てください。 gtime() は date コマンドの内部ルーチンですが、この関数で引数として 与えられた時刻を解釈してシステムクロックを計算する処理を行っているようです。 (前述のライブラリルーチンとは逆の変換ですね) gtime() で正しく処理できた場合、 66 行目の stime() システムコールでシステムクロックの値を設定します。 どうやら date コマンドの Y2K 問題は関数 gtime() の中に閉じているようです。
gtime() の本体は 94 行目から定義されています。 101 行目から 109 行目までは引数として与えられた文字列を並べ替えています。 例えば "9912312359.59" という文字列は "95.9532132199" に並べ替えられます。 (何でこんな処理をしているのか不可解なんですけどね) 110 行目と 111 行目で入力で日付などが省略されている場合に 代わりに使う日付と時刻の情報を取得しておいて、 112 行目から 122 行目までで先ほどの並べ替えた文字列を解釈して、 指定された日付と時刻の情報を取り出しています。 関数 gp() は 152 行目から定義されていますが、 文字列の中の連続した数字2文字を解釈して数値に変換する関数です。 文字列がからの場合は引数として渡されたデフォルト値を返します。 123 行目を見てください。変数 year に値が代入された後、 まだ解釈する文字列が残っている場合はエラーと判断しています。 実行例3の冒頭でコマンドがエラーになったのはここではじかれたようですね。 125 行目から 134 行目は取り出した日付と時刻の情報の 妥当性をチェックしています。135 行目から関数の最後である 148 行目まではシステムクロックの計算をしています。 136 行目を見てください。変数 year に 1900 を加算していますね。 つまり year を4桁の数字としてシステムクロックの計算を行っています。 ちなみに 138 行目で使われている dysize() は 1 年の日数を求める 関数です。これも man page には記述がありませんが libc.a の ライブラリルーチンです。★注2
以上のソースコードの解析からわかったことは、 引数の文字数がもう2文字増えた場合には、 それを年の上2桁と解釈するように変更すれば 2000/1/1 以降の日付と時間も正しく受け付けるようになるはずです。 システムクロックの計算そのものは特に変更する必要はなさそうです。 修正しなければならないのは 123 行目のあたりと 136 行目のあたりの 2箇所ということになりそうですね。
皆さんそれぞれに問題を解決する方法を思いつかれたと思いますが、 一応、私が考えた修正方法をご紹介しておきましょう。 123 行目の後に、gp() を使って更に年数の上2桁の数字を取り込み、 正しく取り込めたら year に加算します。更に 136 行目は上2桁の 数字が取り込めなかった場合にのみ 1900 を加算するようにします。 他にもいろいろ方法が考えられますが、私はソースコードをできるだけ 修正しないで済む方法を選びました。この方法に従ってソースコードを 修正してみましょう。修正要領は実行例3のコメントを参考にしてください。
修正が終わったら、例によって修正内容の確認をしましょう。 問題がなければ data.c をコンパイルします。 コマンドのコンパイルには先ほど紹介したシェルスクリプト cmake を使います。 ここでコンパイルエラーが出る可能性もありますから、もしエラーが表示されたら もう一度、ソースを確認してください。きっとどこかに間違いがあるはずです。
無事コンパイルが終わったら動作確認をしてみましょう。 2桁の年数を指定した場合も、4桁の年数を指定した場合も ちゃんと動いているようですね。実行例3の冒頭でで失敗した、 日付と時刻の設定を受け付けるようになりました。
動作確認が終わったら date コマンドを置き換えます。 先ほどコピーした新しい date コマンドを消して、 今作った date コマンドをコピーします。
以上で date コマンドの Y2K 問題対応は終わりです。 Seventh Edition の時代には、このようにユーザーは誰でも 既存のソースコードに手を入れることをしていたようです。 ソースコードの修正は最小限に抑えるのが Unix ハッカーの流儀でした。 オリジナルバージョンの開発者へ敬意を表する意味もありますし、 無意味な修正をして不必要に diff を大きくすることを避けたこともあります。 修正内容は皆に配らなければなりませんからね。
とはいえ date コマンドのソースを読んだ感想はいかがだったでしょう? 「俺もプログラムをバリバリ書けるわけじゃないけど、これよりは もう少しマシなソースが書けそうな気がするなぁ・・・」と思った方。 あなたの感想はもっともです。実は Seventh Edition のソースコードには Brian Kernihan が毛嫌いしそうなダーティなソースコードが結構あったりします。 あなたと同じ感想を持った当時の学生は「もう少しマシな」プログラムを 書くモチベーションを大いに喚起されたのではないでしょうか。このような リアルで自然発生的な「プログラムを書きたい欲求」はプログラミング講座の 演習課題では喚起しにくいものです。これが当時のプログラマの多く支持を集め、 最良のプログラミング環境と評された Unix の人気の秘密だったのかもしれません。
リアルで自然発生的な「プログラムを書きたい欲求」を感じてしまった 読者の皆さんのために、もう1つプログラミングの事例を紹介しましょう。 ここでは第7回の最後で簡単に取り上げた shutdown コマンドについて、 さらに説明を加えたいと思います。
第7回で紹介した shutdown コマンドは kill システムコールを読んだあと、 sync システムコールを何度か繰り返す(★コラム1)、システムを停止する シーケンスをそのまま書いたような単純なプログラムでした。もちろん 「マルチユーザーモードを終了して、シングルユーザーモードに戻る」ためには このシーケンスさえ実行すれば十分なのですが、コンソール以外にターミナルも サポートした私たちの今のシステムで使うにはこの簡略版 shutdown コマンドは いささか乱暴です。
というのも、ターミナルから(他のコンピュータから Telnet 経由で)他のユーザーが ログインしている可能性があるからです。簡略版コマンドを実行すると、システムは いきなり止まってしまいます。その時もし他のユーザーがログインしていて、 たとえばエディタでファイルの修正を行っていたとしたら、、、修正内容は全部 消えてなくなりますよね?Unix の場合「システムを停止する場合、周りの人に 声をかけてから」というのは昔のお約束でした。このやりかたはあまりにローテクで 騒々しかったのか、後にはターミナルにメッセージを出力する方法が取られるように なりました。そういった理由から皆さんが使ってらっしゃる Unix には shutdown や halt、あるいは reboot といったコマンドが存在して、全ての ログインユーザーにシステムを停止することを通知した後、システムを停止する 機能が備わっています。
そこでSIMH の力を借りてネットワーク経由でログインができるようになった、 私たちのシステムにも同様の機能をサポートする shutdown コマンドを用意する ことにしましょう。一般的な shutdown コマンドが行うシステム停止のための 一連の手順は、概ね次の3つの処理から成ります。
- ログイン中の全てのターミナルを停止メッセージを出力する。
- 全てのユーザープロセスを強制的に終了させる。
- init プロセスを強制終了させてシングルユーザーモードに戻る。
実は、第7回で紹介した shutdown コマンドはこのうちの (3) の処理だけを 切り取ったものでした。今回は残る (1) と (2) について説明します。
まず (1) の「ログイン中の全てのターミナルを停止メッセージを出力する」から。 Unix の場合、ターミナルにメッセージを表示するのは非常に簡単です。 例えば最も単純な例は次のプログラムでしょう。
#include <stdio.h>
int
main()
{
FILE *f;
f = fopen("/dev/tty00", "w");
fprintf(f, "Test Message...\n");
fclose(f);
exit(0);
}
この例でもわかるように、スペシャルファイルのおかげで、 ファイルへの出力とまったく同じ要領で、任意のターミナルへの メッセージ出力ができます。したがって(1) の処理で問題になるのは 「現在ログイン中のターミナルはどれか?」調べることです。 参考になるのが who というコマンドです。★実行例4をご覧ください。
実行例4 who コマンド
# who
root console Oct 25 19:15
dmr tty00 Oct 25 19:16
sys tty01 Oct 25 19:16
bin tty02 Oct 25 19:17
#
who コマンドは現在ロングイン中のユーザーのリストを表示してくれます。 この例ではコンソール以外に3台のターミナルから dmr sys bin という ユーザーがわかります。各々はターミナル /dev/tty00 /dev/tty01 /dev/tty02 からログインしていることもわかりますね? つまり who コマンドのソースコードを読めば、 ログイン中のターミナルを調べる方法がわかるというわけです。 実行例5をご覧ください。
実行例5 who コマンドのソース
# awk '{ printf("%4d ", NR); print $0 }' /usr/src/cmd/who.c
1 /*
2 * who
3 */
4
5 #include <stdio.h>
6 #include <utmp.h>
7 #include <pwd.h>
8 struct utmp utmp;
9 struct passwd *pw;
10 struct passwd *getpwuid();
11
12 char *ttyname(), *rindex(), *ctime(), *strcpy(), *index();
13 main(argc, argv)
14 char **argv;
15 {
16 register char *tp, *s;
17 register FILE *fi;
18
19 s = "/etc/utmp";
20 if(argc == 2)
21 s = argv[1];
22 if (argc==3) {
23 tp = ttyname(0);
24 if (tp)
25 tp = index(tp+1, '/') + 1;
26 else { /* no tty - use best guess from passwd file */
27 pw = getpwuid(getuid());
28 strcpy(utmp.ut_name, pw?pw->pw_name: "?");
29 strcpy(utmp.ut_line, "tty??");
30 time(&utmp.ut_time);
31 putline();
32 exit(0);
33 }
34 }
35 if ((fi = fopen(s, "r")) == NULL) {
36 puts("who: cannot open utmp");
37 exit(1);
38 }
39 while (fread((char *)&utmp, sizeof(utmp), 1, fi) == 1) {
40 if(argc==3) {
41 if (strcmp(utmp.ut_line, tp))
42 continue;
43 #ifdef interdata
44 printf("(Interdata) ");
45 #endif
46 putline();
47 exit(0);
48 }
49 if(utmp.ut_name[0] == '\0' && argc==1)
50 continue;
51 putline();
52 }
53 }
54
55 putline()
56 {
57 register char *cbuf;
58
59 printf("%-8.8s %-8.8s", utmp.ut_name, utmp.ut_line);
60 cbuf = ctime(&utmp.ut_time);
61 printf("%.12s\n", cbuf+4);
62 }
#
これが who のソースコードです。全部で62行の大変短いプログラムですね。 このプログラムのポイントは 35 行目で open している "/etc/utmp" という ファイルです。(パス名は19行目で設定されています) このファイルの open に成功すると 39 行目以降の while ループで utmp という構造体にファイルの内容を read しながら putline() を呼び出して表示を行っています。
この utmp とは何者でしょう? man page で調べてみましょう。
# man utmp
UTMP(5) UNIX Programmer's Manual UTMP(5)
NAME
utmp, wtmp - login records
SYNOPSIS
#include <utmp.h>
DESCRIPTION
The utmp file allows one to discover information about who
is currently using UNIX. The file is a sequence of entries
with the following structure declared in the include file:
struct utmp {
char ut_line[8]; /* tty name */
char ut_name[8]; /* user id */
long ut_time; /* time on */
};
This structure gives the name of the special file associated
with the user's terminal, the user's login name, and the
time of the login in the form of time(2).
The wtmp file records all logins and logouts. Its format is
exactly like utmp except that a null user name indicates a
logout on the associated terminal. Furthermore, the termi-
nal name `~' indicates that the system was rebooted at the
indicated time; the adjacent pair of entries with terminal
names `|' and `}' indicate the system-maintained time just
before and just after a date command has changed the
system's idea of the time.
Wtmp is maintained by login(1) and init(8). Neither of
these programs creates the file, so if it is removed
record-keeping is turned off. It is summarized by ac(1).
FILES
/etc/utmp
/usr/adm/wtmp
SEE ALSO
login(1), init(8), who(1), ac(1)
#
実行例の man page に記述されているとおり、"/etc/utmp" には ターミナルのスペシャルファイル、ユーザーのログイン名、 ログインをした時間など、ログイン中のユーザーに関する情報が 記録されています。したがって「ログイン中の全てのターミナルに メッセージを出力する」には "/etc/utmp" の内容を参照しながら 各々のターミナルにメッセージを出力していけば良いわけです。 例えばこんな感じで、、、
#include <stdio.h>
#include <utmp.h>
int
main()
{
FILE *u, *f;
struct utmp utmp;
char s[16];
u = fopen("/etc/utmp", "r");
while(fread((char *)&utmp, sizeof(utmp), 1, u) == 1) {
if (utmp.ut_name[0] == '\0') continue;
strcpy(s, "/dev/");
strcat(s, utmp.ut_name);
f = fopen(s, "w");
fprintf(f, "Test Message...\n");
fclose(f);
}
fclose(u);
exit(0);
}
これで「ログイン中の全てのターミナル」にメッセージを出力できるはずです。 ちなみに、この utmp というファイルは現在のモダンな Unix でも存在します。 "/var/run/utmp" がそれです。Seventh Edition と全く同じではないのですが、 互換性が維持されているためここで紹介した方法はそのまま使えるはずです。
さて、これでユーザーが使用中の全てのターミナルにメッセージを表示する 方法が分かりました。多くの善良なユーザーは「停止する旨のメッセージ」を 見ただけでログアウトしてくれるでしょう。しかし、中にはログアウトして くれないユーザーもいます。例えば、ログインをしたまま席を離れてしまって いるとか、実行に長時間かかるプログラムの実行中だとか、、、 システムを停止する際、ユーザープロセスが残っているのは望ましく ありません。例えば、システムが停止する瞬間にユーザープロセスが ディスクへの書き込みをしていたとしたら、その書き込んだ内容が どうなるか全く予測がつかないからです。そこでシステムを停止する前に、 「全てのユーザープロセスを強制的に終了させる」必要があります。 つまり (2) の方法を必要になるわけですね。
プロセスを強制的に終了させるためには kill システムコール (kill コマンド) が使えることは、皆さん既に良くご存知ですよね? プロセス ID さえ分かれば kill システムコールで、そのプロセスを 強制的に終了させることができます。では「全てのユーザープロセス」の プロセス ID はどうやって調べるのでしょうか?それを調べるために、 proc という ps コマンドの簡易版を書いてみました。(★実行例6) これはプロセス構造体の中身を表示するプログラムです。 もちろん ps コマンドのソースコードを参考にして書きました。
実行例6 proc -- 簡易版 ps コマンド
# awk '{ printf("%4d ", NR); print $0 }' proc.c
1 #include <stdio.h>
2 #include <a.out.h>
3 #include <sys/param.h>
4 #include <sys/proc.h>
5
6 struct nlist nl[] = {
7 { "_proc" },
8 { "" },
9 };
10
11
12 int
13 main(argc, argv)
14 int argc;
15 char *argv[];
16 {
17 char *kernel = "/unix";
18 char *core = "/dev/mem";
19 struct proc proc;
20 int mem;
21 register int i;
22
23 if (argc > 1) kernel = argv[1];
24
25 nlist(kernel, nl);
26 if (nl[0].n_type == 0) {
27 fprintf(stderr, "No namelist\n");
28 exit(1);
29 }
30 if ((mem = open(core, 0)) < 0) {
31 fprintf(stderr, "No mem\n");
32 exit(1);
33 }
34
35 printf(" S F PRI CPU NICE SIG UID PGRP PID PPID\n");
36
37 lseek(mem, (long) nl[0].n_value, 0);
38 for (i = 0; i < NPROC; i++) {
39 read(mem, (char *)&proc, sizeof(proc));
40
41 if (proc.p_stat == 0) continue;
42
43 printf("%2d", proc.p_stat);
44 printf("%2o", proc.p_flag);
45 printf("%4d", proc.p_pri);
46 printf("%4d", (proc.p_cpu & 0377));
47 printf("%5d", proc.p_nice);
48 printf("%4d", proc.p_sig);
49 printf("%4d", proc.p_uid);
50 printf("%5d", proc.p_pgrp);
51 printf("%6d", proc.p_pid);
52 printf("%6d", proc.p_ppid);
53 printf("\n");
54 }
55
56 close(mem);
57
58 exit(0);
59 }
#
# cc -o proc proc.c
# proc
S F PRI CPU NICE SIG UID PGRP PID PPID
1 3 0 182 20 0 0 0 0 0
1 1 30 0 20 0 0 0 1 0
1 1 30 0 20 0 0 19 19 1
1 0 28 0 20 0 7 20 20 1
1 1 40 0 20 0 0 0 12 1
1 1 40 0 20 0 1 0 15 1
1 1 28 0 20 0 2 21 21 1
1 1 28 0 20 0 3 22 22 1
1 1 28 0 20 0 0 0 23 1
1 1 28 0 20 0 0 0 24 1
1 1 28 0 20 0 0 0 25 1
1 1 28 0 20 0 0 0 26 1
1 1 28 0 20 0 0 0 27 1
3 1 51 25 20 0 0 19 250 19
# ps alx
F S UID PID PPID CPU PRI NICE ADDR SZ WCHAN TTY TIME CMD
3 S 0 0 0 213 0 20 2271 2 4522 ? 47:26 swapper
1 S 0 1 0 0 30 20 2352 8 47140 ? 0:00 /etc/init
1 S 0 19 1 1 30 20 2512 11 47174 co 0:01 -sh
0 S 7 20 1 0 28 20 176 11 5264 00 0:00 -sh
1 S 0 12 1 0 40 20 2642 5 140000 ? 0:00 /etc/update
1 S 1 15 1 0 40 20 3625 10 140000 ? 0:01 /etc/cron
1 S 2 21 1 0 28 20 2711 11 5346 01 0:00 -sh
1 S 3 22 1 0 28 20 3035 11 5430 02 0:00 -sh
1 S 0 23 1 0 28 20 3175 8 5512 ? 0:00 /etc/init
1 S 0 24 1 0 28 20 3271 8 5574 ? 0:00 /etc/init
1 S 0 25 1 0 28 20 3525 8 5656 ? 0:00 /etc/init
1 S 0 26 1 0 28 20 3745 8 5740 ? 0:00 /etc/init
1 S 0 27 1 0 28 20 4041 8 6022 ? 0:00 /etc/init
1 R 0 246 19 75 54 20 5345 20 co 0:01 ps alx
#
この proc というプログラムも全部で 59 行という大変短いプログラムです。 ps コマンドの表示と比較すると表示される情報が少なくなっていますが、 内容は変わりません。
このプログラムのポイントは 30 行目で open している "/dev/mem" という スペシャルファイルと 25 行目で呼び出している nlist というライブラリ関数です。
# man nlist
NLIST(3) UNIX Programmer's Manual NLIST(3)
NAME
nlist - get entries from name list
SYNOPSIS
#include <a.out.h>
nlist(filename, nl)
char *filename;
struct nlist nl[ ];
DESCRIPTION
Nlist examines the name list in the given executable output
file and selectively extracts a list of values. The name
list consists of an array of structures containing names,
types and values. The list is terminated with a null name.
Each name is looked up in the name list of the file. If the
name is found, the type and value of the name are inserted
in the next two fields. If the name is not found, both
entries are set to 0. See a.out(5) for the structure
declaration.
This subroutine is useful for examining the system name list
kept in the file /unix. In this way programs can obtain
system addresses that are up to date.
SEE ALSO
a.out(5)
DIAGNOSTICS
All type entries are set to 0 if the file cannot be found or
if it is not a valid namelist.
#
まず "/dev/mem" ですが、man page にも記述されているように、これは メモリ上のイメージを読み書きするためのスペシャルファイルです。ここでは 実行中のカーネルの変数の内容を直接読むという荒技のために使っています。
# man mem
MEM(4) UNIX Programmer's Manual MEM(4)
NAME
mem, kmem - core memory
DESCRIPTION
Mem is a special file that is an image of the core memory of
the computer. It may be used, for example, to examine, and
even to patch the system. Kmem is the same as mem except
that kernel virtual memory rather than physical memory is
accessed.
Byte addresses are interpreted as memory addresses. Refer-
ences to non-existent locations return errors.
Examining and patching device registers is likely to lead to
unexpected results when read-only or write-only bits are
present.
On PDP11's, the I/O page begins at location 0160000 of kmem
and per-process data for the current process begins at
0140000.
FILES
/dev/mem, /dev/kmem
BUGS
On PDP11's, memory files are accessed one byte at a time, an
inapproriate method for some device registers.
#
Unix のプロセスはカーネルの中では proc という構造体で表現されており、 そのプロセス ID は p_pid というメンバーに記録されます。オリジナル Unix や初期の BSD Unix では、この proc という構造体(プロセス構造体)の 内容を参照するシステムコールは用意されてなかったため、ps コマンドなど プロセスに関する詳細情報を必要とするプログラムは、"/dev/mem" を使って、 その内容を直接参照するという方法を取っていました。 Seventh Edition の場合、プロセス構造体は proc の配列として 宣言されていましたので、このプログラムでは配列の先頭を見つけて (37 行目)、 配列の内容を順次読み出しています。(38行目からのループ) 43行目から53行目までの printf() で、プロセス構造体の内容を 順次表示していることがわかりますね。
ここで問題なるのが proc の配列がメモリ上のどこにあるのか調べる方法です。 このため、このプログラムは nlist() というライブラリ関数を使っています。 man page にも記述があるように、nlist() は name list からエントリを 参照するための関数です。Unix ではオブジェクトファイルをリンクして 実行形式ファイルを生成する際、変数名とそのアドレスを対にしたリストを 作成しますが、このリストを name list と呼びます。つまり、このリストを 使うと、変数名からその変数が置かれている場所を探すことができるわけです。 そこで、このプログラムでは nlist() を使って、カーネルの実行形式ファイル "/unix" を対象に "_proc" という変数のアドレス(位置)を調べています。 (25 行目 変数名の指定は 7 行目) 37 行目の lseek() システムコールを 呼び出す際の第2引数である nl[0].n_value はカーネルの変数 "_proc" が 配置されているアドレスを意味します。
オリジナル Unix や初期の BSD Unix においてプロセスの詳細情報を 調べるためには、このような "/dev/mem" を使う方法が取られました。 しかし、現在のモダンな Unix ではプロセスの詳細情報を調べるための システムコールや /proc ファイルシステムが用意されているので、 この方法は使われません。
ということで (1) と (2) の処理を書くための方法はわかりました。 では (1) (2) (3) を組み合わせて shutdown コマンド第2版を書いてみましょう。 ★実行例7をご覧ください。
実行例7 shutdown コマンド第2版
# awk '{ printf("%4d ", NR); print $0 }' shutdown.c
1 #include <stdio.h>
2 #include <utmp.h>
3
4
5 int
6 broadcast(min)
7 int min;
8 {
9 char buf[BUFSIZ];
10 struct utmp utmp;
11 char *file = "/etc/utmp";
12 char t[50];
13 FILE *u;
14 FILE *f;
15
16 if ((u = fopen(file, "r")) == NULL) {
17 fprintf(stderr, "Cannot open %s\n", file);
18 exit(1);
19 }
20
21 while (fread(&utmp, sizeof(struct utmp), 1, u) != 0) {
22 if (utmp.ut_name[0] == 0) continue;
23
24 strcpy(t, "/dev/");
25 strcat(t, utmp.ut_line);
26
27 if ((f = fopen(t, "w")) == NULL) {
28 fprintf(stderr,"cannot open %s\n", t);
29 exit(1);
30 }
31
32 setbuf(f, buf);
33
34 fprintf(f, "Broadcast Message ...\n\n");
35 if (min > 0) {
36 fprintf(f,
37 "System going down in %d minutes\n\n",
38 min);
39 fprintf(f, "Please finish up\n\n");
40 } else {
41 fprintf(f, "System going down. Bye\n\n");
42 }
43
44 fclose(f);
45 }
46 fclose(u);
47 return(0);
48 }
49
50
51 #include <a.out.h>
52 #include <sys/param.h>
53 #include <sys/proc.h>
54
55 struct nlist nl[] = {
56 { "_proc" },
57 { "" },
58 };
59
60 int
61 killprocess()
62 {
63 char *kernel = "/unix";
64 char *core = "/dev/mem";
65 struct proc proc;
66 int mem, pid, pgid;
67 register int i;
68
69 pid = getpid();
70
71 nlist(kernel, nl);
72
73 if ((mem = open(core, 0)) < 0) {
74 fprintf(stderr, "No mem\n");
75 exit(1);
76 }
77
78 printf("\nKill Process Phase\n\n");
79
80 lseek(mem, (long) nl[0].n_value, 0);
81 while(proc.p_pid != pid) {
82 read(mem, (char *)&proc, sizeof proc);
83 }
84 pgid = proc.p_pgrp;
85
86 lseek(mem, (long) nl[0].n_value, 0);
87 for (i = 0; i < NPROC; i++) {
88 read(mem, (char *)&proc, sizeof(proc));
89
90 if (proc.p_stat == 0) continue;
91 if (proc.p_pgrp == pgid || proc.p_pgrp == 0) continue;
92
93 kill(proc.p_pid, 9);
94 }
95
96 close(mem);
97
98 return(0);
99 }
100
101
102 int
103 main(argc, argv)
104 int argc;
105 char *argv[];
106 {
107 int minutes = 3;
108
109 if (argc > 1) {
110 minutes = atoi(argv[1]);
111 }
112
113 for (;minutes >= 0; --minutes) {
114 broadcast(minutes);
115
116 if (minutes > 0) sleep(60);
117 }
118
119 killprocess();
120
121 printf("****** WAIT FOR SECOND # CHARACTER ******\n");
122 printf("****** DO A SYNC, THEN HALT THE SYSTEM ******\n");
123
124 sync();
125 kill(1, 2);
126 sync(); sync(); sync();
127
128 exit(0);
129 }
#
# cc -o shutdown shutdown.c
#
ここでは (1) の処理は broadcast() という関数に、 (2) の処理は killprocess() という関数に記述しています。 broadcast() の内容は前述のサンプルとほとんど変わりません。 出力するメッセージを「停止メッセージ」に差し替えた程度です。
killprocess() については少し捕捉しておく必要があるでしょう。 前述のプログラム proc では全てのプロセスを走査して、プロセスの 詳細情報を参照しましたが、この中には init や swapper などの システムプロセスも含まれます。ここではユーザープロセス、 すなわちユーザーがログインすることによって生成されたプロセス だけを強制終了させたいので、プロセス毎にシステムプロセスか? ユーザープロセスか?を選別する必要があります。また、プロセス 停止をしている自プロセスを強制終了させることも避けなければ なりません。でないと shutdown コマンド自体が途中で強制終了して しまうことになります。
そのため killprocess() ではプロセス構造体のメンバー p_pgrp に 注目しました。これは今日でいうところのプロセスグループを示す 識別子です。(★コラム) システムプロセスの場合はシステム起動時に プロセスが生成されますので p_pgrp の値は 0 になります。ユーザー プロセスの場合はログインプロセスのプロセス ID の値 (p_pid) が p_pgrp にコピーされます。そこで p_pgrp の値が 0 の場合、あるいは 自プロセスグループの値と一致する場合は、強制終了をさせないように しています。(91行目)
最後に main() ですが、ここでは引数に関する処理をしたのち、 broadcast() と killprocess() を呼び出し、shutdown コマンド第1版と 同じように init プロセスを kill() して、sync() を何度か呼び出して、 プログラムを終了しています。以上で shutdown コマンド第2版は完成です。
※
第7回から3回に渡って御紹介してきたSeventh Editionの実習編は いかがだったでしょうか?特に今回は、既存のコマンドの手直しの方法や 新しいコマンドの作り方の1例を紹介しましたが、これは原則として 現在のモダンなUnixでも使える方法です。
初心者がプログラムを書くときには、今回示したように、 まず作りたいプログラムに必要と思われる処理を考え、 1つずつ個別のプログラムを書いて必要な処理ができるか確かめたうえで、 最後に組み合わせるやり方が間違え難いようです。 つまり「急がば回れ」というわけです。
このようなアプローチを取るには、Seventh Editionは 大変便利なシステムといえるでしょう。なぜなら、 ほとんどのプログラムが非常に単純に記述されているため、 そのソースコードはそのままサンプルコードとしてもそのまま活用できるからです。 Seventh Editionのコマンドは必要最小限の機能しかサポートされていないという 指摘がありますが、それはサンプルコードに求められる単純性との トレードオフが考慮された結果ともいえると思います。 Ken ThompsonやDennis RitchieをはじめとするBTLのメンバーが強く主張する 「Unixスタイル」とはこういうことだったのでしょう。
オリジナルUnixは「プログラムを書くためのシステム」という視点で 最適化がなされた、その目的では非常にバランスのよいシステムだったと思います。 この視点はユーザーに必ずしもプログラミングを求めない実用システムを追求した 後のBSD Unix、あるいは現在のモダンなUnixとは明らかに一線を画するもの でありました。「どちらが優れた考え方か?」を議論するのはナンセンスですが、 開発されてから20年あまり経過したいまでもなお、Seventh Editionは 「プログラムを書くためのシステム」、 あるいは「プログラミングを習得するためのシステム」として 十分価値があると私は思います。
さて、次回からはこの連載本来の歴史的トピックに戻り、 BSD Unixに関して紹介する予定です。
K&R とは書籍の "The C Programming Language" 邦題「プログラミング言語C」 のことです。著者の Brian Kernighan と Dennis Ritchie の頭文字をとって K&R と呼ばれてます。この本は 1978 年に初版が出版されているんですが、 1980 年代の中期にC言語が ANSI X3J11 として標準化されるまで、 この本の初版が事実上の言語仕様標準でした。したがって ANSI C に対して K&R C という呼び方をされることがあります。実際 ANSI C は K&R の仕様を ベースにしたものの、いろいろ機能が追加されてましたので、両者の文法に 結構違いがあります。ちなみに GNU C Compiler では -traditional という オプションを付けると K&R の文法でコンパイルしてくれるはずです。 で、K&R 時代とは ANSI C ができるまでの時代のことを意味しています。 Seventh Edition は正しくこの時代にリリースされましたから、 そのソースコードは全て K&R の文法で記述されているわけです。
dysize() はいわゆる閏年判定のために使われる関数ですが、 Seventh Edition の dysize() では 100 年と 400 年の周期が 考慮されません。したがって厳密な意味では問題がありますが、 100 年と 400 年の周期での誤差が次に発生するのは 2100 年ですので、 (今のところは)事実上無視できる問題です。2100 年になっても Seventh Edition が使われているなんてちょっと考えられないですしね :-p 気になる方は /usr/src/libc/gen/ctime.c を修正して libc.a を 作り直してください。
システムを停止する際 sync を4回繰り返して入力するのは 私の個人的な癖なんですが、一般には sync は3回以上 繰り返して実行するよう教えられることが多いようです。 読者の方から「何故3回以上繰り返す必要があるのですか?」 という質問があったので、ここでお答えしておきます。
そもそも sync コマンドは sync システムコールを呼び出すだけのコマンドで、 この sync システムコールはカーネル内のファイル入出力バッファを強制的に フラッシュするためのシステムコールです。一般的な Unix カーネルでは、 ユーザープロセスで動くアプリケーションプログラムがファイルへの書き込みを 行った場合、その書き込んだ内容は一旦カーネル内の入出力バッファに保管され、 バッファがいっぱいになるなど特定の条件が整うまでディスクへの書き込みは 発生しません。ディスクへの入出力は時間がかかるので、ディスクへの入出力 回数を減らすため、多くのオペレーティングシステムではこのような方法が 使われています。この方法の弱点としてタイミングによっては入出力バッファの 中にはまだ書き込まれていないデータが残っている場合があります。 sync システムコールはこの書き残しデータを強制的にディスクに書き込むための システムコールです。システムを停止するときには、当然書き残しデータも ディスクに書き込まなければならないので、sync コマンド(sync システムコール) を実行する必要があります。「しかし sync システムコールを1回実行すれば カーネル内の全ての入出力バッファをフラッシュ(書き込む)するはずなので、 1回実行するだけで十分なのではないか?」というのは多くの人が感じる 疑問です。
マルチユーザーのオペレーティングシステムである Unix の場合、他の人が 他のターミナルで作業している可能性もあり、またバックグラウンドでは数々の デーモンも動作しています。sync システムコールを実行している最中に、 書き込みの終わったはずの入出力バッファに新たに書き込みが行われる ことも起こるので sync システムコールは何回か実行しないと書き残しが 残ってしまう可能性があるのです。
では「3回実行すれば必ず書き残しは残らないのか?」という質問が 出てきそうですが、これについて調べてみたのですが、、、今のところ 明快な答えは見つけていません。大変高い確率で「書き残しが残らない」 状態にはなるようですが、それが保障されるわけではないようです。 「sync コマンドを3回実行するためにかかる時間が重要である」という説も ありますが、どうにもこじつけの印象は拭えません。とはいえ、海外でも sync コマンドを3回実行するのはお約束になっているようで、おそらく 誰かの経験則がひろまったのではないかと、私は今のところ推測しています。 実は私が新人だった頃同じ質問をしたのですが、先輩から返ってきた答えは、 「1回目は自分のため、2回目は他の人のため、3回目は念のため」でした。 どうやらこの標語のような解答以上の根拠はなさそうです。
PS より明快な真相をご存知の方、ご一報ください。
今日、プロセスグループという名前で知られる概念は Seventh Edition の段階では完全にサポートされてはいなかったようです。 皆さんも確認されているように Seventh Edition のプロセス構造体 (/usr/sys/h/proc.h) には p_pgrp というメンバーが存在しますが、 Lion's Book に掲載されている Sixth Edition のソースコードを見る限り プロセス構造体にはそのようなメンバーは存在しません。したがって p_pgrp は Sixth Edition 以降の改変で追加されたと思われます。 ところがソースコードをどのように探しても p_pgrp の値を変更するための システムコールは Seventh Edition には存在しませんでした。ソースを よーく見てみると、システムコールのテーブル (/usr/sys/sys/sysent.c) の 中に次のようなコメントが見つかりました。
/* 39 = setpgrp (not in yet) */
「じゃあ、一体どこで p_pgrp の値を変更しているのか?」ということを 調べてみたところ、なんと、コンソールやターミナルマルチプレクサの デバイスドライバの共用コード (/usr/sys/dev/mx1.c /usr/sys/dev/pk1.c) の中で変更していました。つまり、コンソールやターミナルからアクセス しているプロセスに対してのみ p_pgrp の値が変更されるようです。
一般にプロセスグループは BSD Unix で初めてサポートされた概念だと、 今日では認知されていますが、真相はこういうことだったんですねぇ。