Article 3991 of fj.unix: Path: news2.sra.co.jp!katsu From: katsu@sra.CO.JP (WATANABE Katsuhiro) Newsgroups: fj.net.www.authoring,fj.unix Subject: atomicity of write(2) (Re: [Q]What's_Lock_File?) Supersedes: Date: 06 Oct 1998 23:47:52 GMT Organization: Software Research Associates, Inc., Japan Lines: 398 Message-ID: <6vepkg$l9d$1@sranhh.sra.co.jp> References: <4rfj6i$e1r@jupiter.ntt-it.co.jp> NNTP-Posting-Host: sras49.sra.co.jp In-reply-to: Hiroaki SHIROUZU's message of 4 Jul 1996 05:01:06 GMT Originator: katsu@sras49 Xref: news2.sra.co.jp fj.net.www.authoring:215 fj.unix:3991 あまりのフォローの遅さが議論の妨げになっていたらごめんなさい。 ホントに遅すぎて newsgroup がなくなってしまった...こりゃあかん。 投稿時に化けた を supersedes します。 記事 <4rfj6i$e1r@jupiter.ntt-it.co.jp> で、 shirouzu@ntt-it.co.jp (Hiroaki SHIROUZU) さんいはく > Newsgroups: fj.net.infosystems.www.authoring,fj.unix > Date: 4 Jul 1996 05:01:06 GMT > In article , sakakura@elelab.nsc.co.jp > says... > >> ・1回のwrite()で書かれたものはatomicに書き込まれる。 > >そんなこと無いです。atomicに行なわれるものは大変まれです。 > >たまたまwriteで要求されたものが、1回のDMAで済む場合などに限られます。 > > うーん、必ずしも「1回のDMAで済む場合などに限られる」と断言は > できないと思いますよ。 > > 実装依存な部分かもしれないので、歯切れが悪いのですけど、 > 大抵のUNIXは、ローカルディスクへの書き込みでは、write(2)が > 始まると同時に、カーネル内の(メモリ上の)該当inodeをロックし、 > 書き込みが終了するまで、ロックを外さないのでは? > > あ、もちろん書き込みが終了するといっても、実際のディスクへの > 転送とは無関係ですよ。 ローカルなファイルシステムへの書き込み以外では atomic でないことは 確認済み [1] なので、ローカルなファイルシステムに限った議論をします。 §1.エラーが起きたときの write(2) の返り値の variations に関して write() の返り値を -1 と比較するコードを見かけた時代もありました。 仮に、ローカルディスクへの write(2) は必ず全部書き込まれてから 終わる(決して中途で終わらない)ものとしましょう。その場合でも、 write() の結果を -1 と比較するコードは書くべきではありません。 過去でもそうであったし、現在はなおさらだと思います。 write(2) が file system full 等のエラーを引き起きた時、その返り値は NEWS-OS 4.2.1R -1 ASCII UX/4.3BSD -1 UNIX version 6 -1 (参考文献 [LIONS]) System III, 古い Sys V -1 (参考文献 [SYSCALL]) | tmpfs へ write ufs へ write --------------+--------------------------------- SunOS 5.X | 書いた byte 数 書いた byte 数 SunOS 4.1.4 | 書いた byte 数 書いた byte 数 SunOS 4.1.2 | 書いた byte 数 -1 POSIX 標準 書いた byte 数 NeXT Mach 3.3 write(2) 内で pause する。(O_NONBLOCK の時も。) のようにいろいろです。特に最近は各種標準のおかげで、書いた byte 数を 返すシステムがほとんどです。よって、少なくとも、エラー検出を (素朴に)試みるコード if (write(...) == -1) { /* Error should be detected. */ } は、期待どおり動かないとか、移植性が低いと言えましょう。一方、 if (write(..., write_size) != write_size) { /* Error should be detected and errno should be set. */ } は、(write(2) が中途で終わらないと本当に確信できるほどの知識がある 技術者なら!)エラーが起きたらしいことはわかりますが、errno(3) に 有意な値が設定されません。エラーの検出をしたければ、よしんば write が中途で終わる場合を気にしないとしても、必然的に、 r = 0; while (write_size > 0) { r = write(fd, buf, write_size); if (r == -1) { break; } write_size -= r; buf += r; } if (r == -1) { /* Error will be detected. `errno' will be set. */ } のような感じに記述せざるを得ないでしょう。 §2 write(2) の atomic 性が完全には実装されてないことがある 以下では、ローカルなファイルシステムの代表として ufs を取り上げ、 さらに簡単のため、ENOSPC などのエラーが起きない場合だけを考えます まず、database や並行プログラミング等で atomic という言葉を使う場合、 これはとても強い性質を語っていて、それに習うと、 (a) ufs に対する write(2) は atomic operation である。 (b) ufs に対する write(2) は、全部書き込まないうちは終了しない。 には区別があることを確認しておきます。一例を出すと、条件 (b) を 保っても、write が他の操作と干渉しあっては atomic とはいえません。 もちろん (a) は (b) を包含しています。一連の議論(引用した記事)での atomic の使われ方は、多分に (b) でありました。この記事では区別して、 両者を考察しておきます。 (b) は信じている人が多く、私もその中の一人です。 「一回の DMA で済む場合」のみという主張(引用した記事の内部を参照) は、珍しい部類だと思います。(DMA がどれほど関係するのかしら。) 著名な UNIX hacker もかつて fj.unix で、(b) は正しい旨の投稿 [2] を してます。しかし、fj でこれを論拠を示して説明した例はないでしょう。 (知ってても書けない場合もありえますね...。)ぜひ誰か説明されたし。 (a):atomic 性については、仕様上はそうですが、実装上は例外があります。 write(2) の仕様としては (a) を目指してるはずでしょう。引用した記事の、 inode を lock してるという主張も、普通の UNIX なら全くその通りです。 ところが、多くの実装で以下のような例外が見られました。ufs の ファイルに対してでも、実装上という意味では write(2) は atomic でないというのが私の結論です。 #define BIGSIZE (98305に代表される数十KBから数MBの大きさ) fp = creat("./remove_me", 0644); data = malloc(BIGSIZE); if (fork()) { write(fp, data, BIGSIZE); } write(fp, data, BIGSIZE); もし write(2) が atomic ならば、上のコードは BIGSIZE * 2 の大きさの ファイル ./remove_me を生成するはずです。(親と子の fp が system file descriptor を共有するのが fork(2) の semantics である点に強く注意。) しかし、ufs 上のディレクトリで実行してみると、BIGSIZE だけのファイルが できることがよく観測されます。記事の最後に実際のコードを付録します。 この bug はかなり広範に見られた振舞いなので、write(2) と fork(2)を 使う時(親と子プロセスで file descriptor を共有する時)は要注意です。 参考文献: [1] Subject: UNIX read/write; Message-ID: ; http://www.sra.co.jp/people/katsu/article/92.txt; [2] Subject 不詳の fj.unix の記事; Message-ID: <6223@icsts1.osaka-u.ac.jp>; From: saitoh@icsts1.osaka-u.ac.jp (SAITOH Akinori); どうも古すぎて、JAIST の NetNews archive でさえ見つからない。 http://www.sra.co.jp/people/katsu/article/75.txt で一部引用。 [LIONS] "Lions' Commentary on UNIX 6th Edition with Source Code"; John Lions 著; 岩本信一訳; アスキー; ISBN 4-7561-1844-5; write(2) の途中でディスクが一杯になった場合に関しては、 write() -> rdwr() -> writei() -> bmap() -> alloc() という呼びだし列を順次参照。 [SYSCALL] "UNIXシステムコール・プログラミング"; Marc J. Rochkind 著; 福崎俊博訳; アスキー; ISBN 4-87148-260-X; write(2) の途中でディスクが一杯になった場合に関しては、p.80: > 注4: システムIIIのいくつかのバージョンや,おそらくほかの > バージョンでもそうだろうが,マニュアルが間違っている.部分的な > 書き込みが起こると,部分的なカウントの代わりに -1 が返される > のである.... ---------------- 以上で write(2) の atomic 性の議論は終りで、ここからは、 非 atomic 性を露呈させた上のコードに関して、追加の情報です。 UX/4800 Release 11.4(SysV), ASCII UX/4.3BSD, NEWS-OS [34].X, SunOS [34].X, NeXT Mach 3.3 等で、この非 atomic 性を観測しました。 加えて SunOS 5.3 でも見られますが、一方で最近の SunOS 5.4 とか 5.5.1 では観測できません。4.4BSD に由来する FreeBSD, NetBSD 等でも 非 atomic 性には逢わないようです。Linux も非 atomic 性を示しません。 実は上のコードが非 atomic な振舞いを示す場合、もっとわかりやすい 不具合も明らかになります。BIGSIZE だけのファイルができた場合に、 write の直後に lseek(fd, 0, SEEK_CUR) を問い合わせてみると、 子か親のいずれかでは BIGSIZE * 2 が返ってくるのです。 BIGSIZE がごく小さいうちは、どんな実装でも正しく atomic な振舞いを 示します。非 atomic な振舞いを示し始める閥値は、 ・blocksize が 8192 ならば 98305 ・blocksize が 4096 ならば 49153 であることがわかっています。これらの魔術的数字が何かと言うと、 98305 = NDADDR * 8192 + 1 49153 = NDADDR * 4096 + 1 (NDADDR については 参照)ですから、問題は、FFS で 2重間接ブロックを割り当て始める時と密に関係してると想像できます。 ufs 以外でも、 (A) SunOS の tmpfs に write する場合 (B) ufs に NFS を通して write する場合 にも非 atomic 性が見られますが、様相は異なり、問題を起こし始める 閥値が大きくなり、かつ変化して具体的な値を決定できなくなります。 ufs への write であっても、 (C) NEWS-OS で、mount(8) に delay option(遅延書き込み:NEWS-OS 特有) を指定した場合 も同様に様相が変わり、閥値が大きくなって、かつ変化するようになります。 根拠なく個人的な予想をすると、(A),(B),(C) いずれもバッファや キャッシュの概念と関係深いことから、バッファやキャッシュがあふれて ブロック割り当てが実際に起きた時点で atomic 性が壊れているものと 想像しています。 -- 渡邊克宏@SRA [付録] 上で示した write(2) の非 atomic 性を再現するプログラム fdw.c : コンパイルして、 % ./fdw writeする大きさ のように実行してください。./remove_me というファイルが出来ますが、 これは手動で rm する必要があります。 /* * write(2) on same file descriptor shared between parent and child. * * usage: fdw write-size-in-byte * * Try with big write-size. * On 4.3BSD system, you can observe the parent and the child * write successfully and lseek tells you the tail is 2 * write-size, * but actual file size is just write-size. This means write(2) is * not atomic operation. * * * Almost equivalent to the following script: #!/bin/sh wsize=$1 exec > ./remove_me dd if=/dev/zero bs=$wsize count=1 & dd if=/dev/zero bs=$wsize count=1 wait * * * Written by WATANABE Katsuhiro . * Freely redistributable. */ #include #include #ifdef _POSIX_SOURCE #include #else #include #endif _POSIX_SOURCE #include #ifdef SEEK_CUR #else #ifdef L_INCR #define SEEK_CUR L_INCR #else #define SEEK_CUR 1 #endif #endif #define TESTFILE "./remove_me" extern char *malloc(); int main(argc, argv) int argc; char **argv; { int f; int s, writtenSize; long i; char *datap, *datac; struct stat status; if (argc != 2) { fprintf(stderr, "Usage: %s write-size\n", argv[0]); exit(9); } s = atol(argv[1]); if ((datap = malloc(s * 2)) == NULL) { fprintf(stderr, "malloc(): cannot allocate memory.\n"); exit(1); } datac = datap + s; for (i = 0; i < s; i++) { datap[i] = 0; datac[i] = 1; } if ((f = creat(TESTFILE, 0644)) < 0) { perror("creat()"); exit(2); } switch (fork()) { case -1: /* fail */ perror("fork()"); exit(3); case 0: /* child */ fprintf(stderr, "Child: at %ld.\n", lseek(f, 0, SEEK_CUR)); writtenSize = write(f, datac, s); if (writtenSize == -1) { perror("Child: write()"); } else if (writtenSize != s) { fprintf(stderr, "Child: written partially (%ld).\n", writtenSize); } fprintf(stderr, "Child: at %ld.\n", lseek(f, 0, SEEK_CUR)); if (close(f)) { perror("Child: close()"); } exit(0); default: /* parent */ fprintf(stderr, "Parent: at %ld.\n", lseek(f, 0, SEEK_CUR)); writtenSize = write(f, datap, s); if (writtenSize == -1) { perror("Parent: write()"); } else if (writtenSize != s) { fprintf(stderr, "Parent: written partially (%ld).\n", writtenSize); } fprintf(stderr, "Parent: at %ld.\n", lseek(f, 0, SEEK_CUR)); wait(0); if (fstat(f, &status)) { perror("fstat()"); exit(4); } fprintf(stderr, "File size %ld.\n", status.st_size); if (close(f)) { perror("Parent: close()"); } } } /* Results * NEWS-OS 4.2.1C NEWS-830 (n2) memory 8MB on /var/tmp(4096/512): write works atomicly on or less than 49152. * NEWS-OS 4.2.1C NEWS-830 (n2) 8MB on /tmp(8192/1024 delay option): about 760k(changeable) * NEWS-OS 4.2.1C NEWS-830 (n2) 8MB on /usr(8192/1024): 98304 * NEWS-OS 4.2.1C NEWS-830 (n2) 8MB on /(8192/1024): 98304 * NEWS-OS 4.2.1C NEWS-1720 (n229) 16MB on /(8192/1024): 98304 * NEWS-OS 4.2.1C NEWS-1720 (n229) 16MB on /var(8192/1024): 98304 * NEWS-OS 4.2.1C NEWS-1720 (n229) 16MB on /usr(8192/1024): 98304 * NEWS-OS 4.2.1C NEWS-1720 (n229) 16MB on /home(8192/1024): 98304 * NEWS-OS 4.2.1C NEWS-1720 (n229) 16MB on /var(8192/1024): about 1.8MB(changeable) * NEWS-OS 4.2.1C NEWS-1720 (n229) 16MB on /home(8192/1024 delay): about 2MB (changeable) * NEWS-OS 4.2.1C NEWS-1860 (n330) memory unknown /var/tmp(4096/512): 49152 * NEWS-OS 4.1C NEWS-1860 (svh) 16MB /tmp(8192/1024 delay): about 1.4M(changeable) * NEWS-OS 4.1C NEWS-1860 (svh) 16MB /var/tmp(8192/1024 delay): about 1.4M(changeable) * NEWS-OS 4.2.1R NEWS-3860 (n265) 32MB /tmp(8192/1024 delay): about 5MB(changeable). * NEWS-OS 4.2.1R NEWS-3860 (n265) 32MB /var/tmp(8192/512): 98304 * NEWS-OS 4.2.1R NEWS-3860 (n265) 32MB /home(8192/512): 98304 * NEWS-OS 4.2.1R NEWS-3470 (n419) 16MB 102396Kvirtual: about 2MB(changeable). * SunOS 4.1.2-JLE1.1.2 SS2 (s49) 32MB /var(8192/1024): 98304 * SunOS 4.1.2-JLE1.1.2 SS2 (s49) 32MB /tmp(8192/1024): 98304 * SunOS 4.1.2-JLE1.1.2 SS2 (s49) 32MB with 66088virtual on tmpfs: about 8330582(changeable) * SunOS 4.1.4-JLE1.1.4 SS5 (s25) 64MB 202920Kvirtual on tmpfs : about 10MB(very changeable). * SunOS 4.1.4-JLE1.1.4 SS5 (s25) 64MB 202920Kvirtual /(8192/1024): 98304 * SunOS 5.3 SS5 (s62) 64KB 377373Kvirtual on tmpfs: about 16MB(changeable) * SunOS 5.3 SS5 (s62) 64KB 377373Kvirtual /var(8192/1024): 98304 * SunOS 5.3 SS5 (s62) 64KB 377373Kvirtual /export/home(8192/1024): 98304 * ASCII UX/4.3BSD (vb) microVAX 3800 32MB on /tmp(8192/1024): 98304 * ASCII UX/4.3BSD (vb) microVAX 3800 32MB on /be(8192/1024): 98304 * ASCII UX/4.3BSD (vb) microVAX 3800 32MB on /bb(8192/1024): 98304 * NeXT Mach 3.3 (watchman) 32MB /(8192/1024): 843776 * NeXT Mach 3.3 (watchman) 32MB /export(8192/1024): 843776 * UX/4800 Release 11.4 Rev.D (SysV) 48MB /(ufs 8192/1024): about 750K(changeable) * FreeBSD 2.2.6 ThinkPad310 32MB : always atomic(<20M) * Linux 2.0.33 ThinkPad310 32MB : always atomic(<20M) * SunOS 5.4 SS5 (s70) 64MB : always atomic(<50M) * SunOS 5.5.1 SS20 (s82) 128MB : always atomic(<100M) * NFS cases * s49 to n265:/mnt(8192/1024) : 40959 * n229 to n265:/mnt : about 2MB * n229 to s49:/tmp : about 2MB * s70 to s49:/tool : always atomic(<50M) * s49 to s70:/var(8192/1024): 40959 * mount(8) wsize option has no effect in every case. */