C言語: write(2)の正しい使い方

ここにwriteシステムコールの正しい使い方を書き記しておく。

学校の課題(OS演習)で、open, read, write, close等のシステムコールを使用した課題が出された。システムコールなので当然失敗する事が有り、エラーチェックを正しく行う必要が有るのだが、writeについてはweb上に有る資料も使い方を間違っているモノが多かった。僕も課題では間違えて再提出をくらってしまった。なので、ここにwriteシステムコールの正しい使い方を書き記しておく。

まずは $man 2 write の一部を抜粋してみる。

NAME
       write - write to a file descriptor

SYNOPSIS
       #include 

       ssize_t write(int fd, const void *buf, size_t count);

DESCRIPTION
       write writes up to count bytes to the file referenced by the file descriptor fd from the buffer
       starting at buf.  POSIX requires that a read() which can be proved to occur after a write() has
       returned returns the new data.  Note that not all file systems are POSIX conforming.

RETURN VALUE
       On  success, the number of bytes written are returned (zero indicates nothing was written).  On
       error, -1 is returned, and errno is set appropriately.  If count is zero and the file  descrip-
       tor  refers to a regular file, 0 will be returned without causing any other effect.  For a spe-
       cial file, the results are not portable.

注意したいのが返り値の部分で、write(fd, buf, n);とした場合に必ずwriteが n byte(s) 書き込んでくれるとは限らないので有る。

例えば1024書き込もうとした時に一回目のwriteが256を返した時は、2回目のwriteをどのように呼べば良いのか考えてみよう。答えはこうだ。

(1) write(fd, buf, 1024) => 256
(2) write(fd, buf+256, 768) => 768

この様なケースはローカルなファイルシステムに書き込む場合には起こる事は少ないが、ネットワークを介するwrite等の場合は十分起こりうる。結局、writeを正しく使うコードは以下の様になる。

/* 注: 下部により簡潔なコードが有るのでこれは使わないで下さい */
int n = bytes_to_write;
int w = 0;
int r = 0;
while ((w < n) && ((r = write(fd, buf+w, n-w)) >= 0)) {
    w += r;
}

if (r < 0) {
    close(fd);
    perror("write failed");
    exit(EXIT_FAILURE);
}

自戒もこめて記事にしました。

2006/05/11: より簡潔なコード

改善案。

const char *p = buf;
const char * const endp = p + size;

while (p < endp) {
  int num_bytes = write(fd, p, endp - p);
  if (num_bytes < 0) {
    perror("write failed");
    break;
  }

  p += num_bytes;
}

2006/06/08: EINTRに対処する

システムコール処理中に割り込みが起こった場合、そのシステムコールは一時中断されてしまう。この時、システムコール呼び出しをもう一度やりなおす必要が有る。割り込みによって中断された場合errnoにはEINTRが入っているのでそれをチェックする。

const char *p = buf;
const char * const endp = p + size;

while (p < endp) {
  const int num_bytes = write(fd, p, endp - p);
  if (num_bytes < 0) {
    if (errno == EINTR) continue;
    perror("write failed");
    break;
  }

  p += num_bytes;
}

Schemeの処理系の一つであるGaucheではこれを上手くマクロで処理していたので紹介しておく。

/*
 * gauche.h - Gauche scheme system header
 *
 *   Copyright (c) 2000-2005 Shiro Kawai, All rights reserved.
 *
 *   Redistribution and use in source and binary forms, with or without
 *   modification, are permitted provided that the following conditions
 *   are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 *   3. Neither the name of the authors nor the names of its contributors
 *      may be used to endorse or promote products derived from this
 *      software without specific prior written permission.
 *
 *   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *   "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 *   A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 *   OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 *   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 *   TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 *   PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 *   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 *   NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 *   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 *  $Id: gauche.h,v 1.442 2005/10/13 08:14:13 shirok Exp $
 */

/* System call wrapper */
#define SCM_SYSCALL3(result, expr, check)       \
  do {                                            \
    (result) = (expr);                          \
    if ((check) && errno == EINTR) {            \
      ScmVM *vm__ = Scm_VM();                   \
      errno = 0;                                \
      SCM_SIGCHECK(vm__);                       \
    } else {                                    \
      break;                                    \
    }                                           \
  } while (1)

#define SCM_SYSCALL(result, expr) \
  SCM_SYSCALL3(result, expr, (result < 0))

手元で使用する場合には、vm__変数が登場する2行を削除すれば良さそうだ。これを以下の様に用いる。

const char *p = buf;
const char * const endp = p + size;

while (p < endp) {
  int num_bytes;
  SCM_SYSCALL(num_bytes, write(fd, p, endp - p));
  if (num_bytes < 0) {
    perror("write failed");
    break;
  }

  p += num_bytes;
}

全てのシステムコール呼び出しは割り込まれる可能性が有るので、常にerrno == EINTRをチェックするのが望ましい。僕が経験したものだと、サーバーアプリケーションを書いていた時にaccept(2)がerrno == EINTRで停止するという事が起きた。高負荷にならないと起きなかったが、逆に言うとこういう細かい事をきちんと対処しておかないと後々痛い目に合うという事だろうか。以来、システムコールの呼び出しの際にはこのマクロを常に併用するように心がけている。

2006/06/14: EPIPEに対処する

ネットワークアプリケーションを作成する場合等にはさらに気を付けるべきことがある。それはSIGPIPEシグナルだ。man 2 writeを見るとEPIPEというエラーが有る事がわかる。

これは書き込む先がソケットであり、そのソケットがクライアント側で既にclose(2)されている場合に起こるエラーなのだが、注意したいのはこのエラーが起こる場合には同時にSIGPIPEシグナルが送出されるという点だ。

SIGPIPEシグナルを受け取った場合、プロセスはデフォルトの動作として自分自身をストップさせてしまう。これは連続稼働すべきサーバーアプリケーションを作る場合には非常にまずい。

Programming UNIX Sockets in C - Frequently Asked Questionsによると、SIGPIPEは単に無視するべきだそうだ。サーバーアプリケーションでwrite(2)を使用する場合には必ずSIGPIPEを無視するべきだ。

Kazuki Ohta (or Ota)
Kazuki Ohta (or Ota)
Co-Founder & CEO

I’m a Japanese entrepreneur with the heart of computer science. My Japanese name is 太田 一樹.