可以写入(2) return 0 个字节*,如果写入了怎么办?

Can write(2) return 0 bytes written*, and what to do if it does?

我想实现一个适当的 write(2) 循环,它接受一个缓冲区并不断调用 write 直到整个缓冲区被写入。

我想基本的方法是这样的:

/** write len bytes of buf to fd, returns 0 on success */
int write_fully(int fd, char *buf, size_t len) {
  while (len > 0) {
    ssize_t written = write(fd, buf, len);
    if (written < 0) {
      // some kind of error, probably should try again if its EINTR?
      return written;
    }
    buf += written;
    len -= written;
  }
  return 0;
} 

...但这提出了一个问题,即 write() 是否可以有效地写入 return 0 个字节以及在这种情况下该怎么做。如果这种情况持续存在,上面的代码只会在 write 调用上热旋转,这似乎是个坏主意。只要 other 不是零return,你就在取得进步。

write 的手册页有点含糊。它说,例如:

On success, the number of bytes written is returned (zero indicates nothing was written).

这似乎表明在某些情况下是可能的。只有一种这样的场景被明确指出:

If count is zero and fd refers to a regular file, then write() may return a failure status if one of the errors below is detected. If no errors are detected, or error detection is not performed, 0 will be returned without causing any other effect. If count is zero and fd refers to a file other than a regular file, the results are not specified.

上面避免了这种情况,因为我从来没有用 len == 0 调用 write。还有很多其他情况无法写入,但通常它们都有与之关联的特定错误代码。

文件本身将由命令行中给出的 path/name open 编辑。所以它 通常 是一个常规文件,但用户当然可以传递诸如管道之类的东西,进行输入重定向,传递诸如 /dev/stdout 之类的特殊设备等等。我至少控制了 open 调用并且 O_NONBLOCK 标志未传递给打开。我无法合理地检查所有文件系统、所有特殊设备的行为(即使我可以,也会添加更多),所以我想知道如何以合理和一般的方式处理这个问题方式.


* ...对于非零缓冲区大小。

这取决于文件描述符指的是什么。当您在文件描述符上调用 write 时,内核最终会调用相关文件操作向量中的写入例程,该向量对应于文件描述符所引用的底层文件系统或设备。

大多数普通文件系统永远不会 return 0,但设备几乎可以做任何事情。您需要查看相关设备的文档以了解它可能执行的操作。设备驱动程序写入 return 0 字节是 合法的 (内核不会将其标记为错误或任何其他内容),如果是,写入系统调用将 return 0.

我认为唯一可行的方法(除了完全忽略这个问题,根据文档似乎是要做的事情)是允许 "spinning in place"。

您可以实施重试计数,但如果 极不可能 “0 return 具有非零长度”是由于某些瞬态情况 - LapLink 队列可能已满;我记得 那个 驱动程序做了奇怪的事情 - 循环可能会如此之快以至于任何 合理的 重试次数都会被淹没;如果您有 其他 设备需要 non-negligible 时间 return 0.

所以我会尝试这样的事情。您可能想改用 gettimeofday() 以获得更高的精度。

(我们正在为似乎几乎没有发生机会的事件引入可忽略的性能惩罚)。

/** write len bytes of buf to fd, returns 0 on success */
int write_fully(int fd, char *buf, size_t len) {
  time_t timeout = 0;
  while (len > 0) {
    ssize_t written = write(fd, buf, len);
    if (written < 0) {
      // some kind of error, probably should try again if its EINTR?
      return written;
    }

      if (!written) {
          if (!timeout) {
              // First time around, set the timeout
              timeout = time(NULL) + 2; // Prepare to wait "between" 1 and 2 seconds
              // Add nanosleep() to reduce CPU load
          } else {
              if (time(NULL) >= timeout) {
                  // Weird status lasted too long
                  return -42;
              }
          }
      } else {
          timeout = 0; // reset timeout at every success, or the second zero-return after a recovery will immediately abort (which could be desirable, at that).
      }

    buf += written;
    len -= written;
  }
  return 0;
}

Posix 为支持 non-blocking 操作的管道、FIFO 和 FD 定义它,在 nbyte(第三个参数)为正且调用不是的情况下中断:

if O_NONBLOCK is clear ... it shall return nbyte.

换句话说,它不仅不能 return 0,除非 nbyte 为零,在上述情况下,它也不能 return 短长度。

TL;DR 总结

除非你不顾一切地调用未指定的行为,否则你不会从 write() 得到零结果,除非你尝试写入零字节(问题中的代码避免这样做).

POSIX 说:

write() 的 POSIX 规范涵盖了这个问题,我相信。

The write() function shall attempt to write nbyte bytes from the buffer pointed to by buf to the file associated with the open file descriptor, fildes.

Before any action described below is taken, and if nbyte is zero and the file is a regular file, the write() function may detect and return errors as described below. In the absence of errors, or if error detection is not performed, the write() function shall return zero and have no other results. If nbyte is zero and the file is not a regular file, the results are unspecified.

这说明如果您请求写入零字节,您可能会得到一个 return 零值,但有一些警告 — 它必须是一个常规文件,并且您可能会得到一个如果检测到 EBADF 之类的错误,则出错,并且未指定如果文件描述符不引用常规文件会发生什么。

If a write() requests that more bytes be written than there is room for (for example, [XSI]⌦ the file size limit of the process or ⌫ the physical end of a medium), only as many bytes as there is room for shall be written. For example, suppose there is space for 20 bytes more in a file before reaching a limit. A write of 512 bytes will return 20. The next write of a non-zero number of bytes would give a failure return (except as noted below).

[XSI]⌦ If the request would cause the file size to exceed the soft file size limit for the process and there is no room for any bytes to be written, the request shall fail and the implementation shall generate the SIGXFSZ signal for the thread. ⌫

If write() is interrupted by a signal before it writes any data, it shall return -1 with errno set to [EINTR].

If write() is interrupted by a signal after it successfully writes some data, it shall return the number of bytes written.

If the value of nbyte is greater than {SSIZE_MAX}, the result is implementation-defined.

这些规则并没有真正授予 return 0 的权限(尽管学究可能会说太大的 nbyte 值可能被定义为 return 0)。

When attempting to write to a file descriptor (other than a pipe or FIFO) that supports non-blocking writes and cannot accept the data immediately:

  • If the O_NONBLOCK flag is clear, write() shall block the calling thread until the data can be accepted.

  • If the O_NONBLOCK flag is set, write() shall not block the thread. If some data can be written without blocking the thread, write() shall write what it can and return the number of bytes written. Otherwise, it shall return -1 and set errno to [EAGAIN].

…details for obscure file types — a number of them with unspecified behaviour…

Return value

Upon successful completion, these functions shall return the number of bytes actually written to the file associated with fildes. This number shall never be greater than byte. Otherwise, -1 shall be returned and errno set to indicate the error.

因此,由于您的代码避免尝试写入零字节,只要 len 不大于 {SSIZE_MAX},并且只要您不写入模糊的文件类型(如共享内存对象或类型化内存对象)你不应该看到零 returned by write().


POSIX 理由说:

稍后在 write() 的 POSIX 页面的“基本原理”部分中,有以下信息:

Where this volume of POSIX.1-2008 requires -1 to be returned and errno set to [EAGAIN], most historical implementations return zero (with the O_NDELAY flag set, which is the historical predecessor of O_NONBLOCK, but is not itself in this volume of POSIX.1-2008). The error indications in this volume of POSIX.1-2008 were chosen so that an application can distinguish these cases from end-of-file. While write() cannot receive an indication of end-of-file, read() can, and the two functions have similar return values. Also, some existing systems (for example, Eighth Edition) permit a write of zero bytes to mean that the reader should get an end-of-file indication; for those systems, a return value of zero from write() indicates a successful write of an end-of-file indication.

因此,尽管 POSIX(如果不是全部的话,在很大程度上)排除了 write() 为零 return 的可能性,但相关系统的现有技术确实具有 write() return零。

我会说整个问题都是不必要的。你只是太小心了。您希望该文件是常规文件,而不是套接字,不是设备,不是 fifo 等。我会说 return 从 write 到不相等的常规文件to len 是一个不可恢复的错误。不要试图修复它。你可能填满了文件系统,或者你的磁盘坏了,或者类似的东西。 (这一切都假设你没有配置你的信号来中断系统调用)

对于常规文件,我不知道是否有任何内核尚未执行所有必要的重试以写入您的数据,如果失败,则错误很可能严重到应用程序无法修复它.如果用户决定将 non-regular 文件作为参数传递,那又如何呢?这是他们的问题。他们的脚和他们的枪,让他们开枪。

通过尝试在您的代码中修复此问题,您更有可能通过创建无限循环吃 CPU 或填充文件系统日志或只是挂起而使事情变得更糟。

不处理 0 或其他短写,只在除 len 之外的任何 return 上打印错误并退出。一旦您从用户那里获得了正确的错误报告,该错误报告实际上有写入失败的正当理由,请修复它。这很可能永远不会发生,因为几乎每个人都会这样做。

是的,有时阅读 POSIX 并找到边缘情况并编写代码来处理它们很有趣。但是操作系统开发人员不会因为违反 POSIX 而被送进监狱,所以即使您的聪明代码完全符合标准所说的内容,也不能保证事情会一直有效。有时最好把事情做好,并在事情破裂时依靠良好的陪伴。如果常规文件写入开始 return 很短,那么您将在一家如此优秀的公司工作,很可能会在您的任何用户注意到之前很久就修复它。

N.B。大约 20 年前,我从事文件系统实现工作,我们试图成为其中一项操作行为的标准律师(不是 write,但同样的原则适用)。我们的 "it is legal to return the data in this order" 被崩溃的应用程序的大量错误报告所淹没,这些错误报告以某种方式期望事情发生,最后只是修复它而不是在每个错误报告中进行相同的战斗更快。对于任何想知道的人来说,当时(可能今天仍然)有很多东西期望 readdir 到 return ... 作为目录中的前两个条目(在至少在当时)没有任何标准强制要求。

我个人使用了几种方法来解决这个问题。

下面是三个示例,它们都希望在阻塞描述符上工作。 (也就是说,他们认为 EAGAIN/EWOULDBLOCK 是一个错误。)


当保存重要的用户数据时,没有时间限制(因此写入不应该被信号传递中断),我更喜欢使用

#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int write_uninterruptible(const int descriptor, const void *const data, const size_t size)
{
    const unsigned char       *p = (const unsigned char *)data;
    const unsigned char *const q = (const unsigned char *)data + size;
    ssize_t                    n;

    if (descriptor == -1)
        return errno = EBADF;

    while (p < q) {

        n = write(descriptor, p, (size_t)(q - p));
        if (n > 0)
            p += n;
        else
        if (n != -1)
            return errno = EIO;
        else
        if (errno != EINTR)
            return errno;
    }

    if (p != q)
        return errno = EIO;

    return 0;
}

如果发生错误(EINTR 除外),或者 write() return 为零或 -1 以外的负值,这将中止。

因为上面没有理智的理由 return 部分写入计数,如果成功则改为 returns 0,否则为非零 errno 错误代码。


写入重要数据时,如果有信号传递,写入会被打断,界面有点不同:

size_t write_interruptible(const int descriptor, const void *const data, const size_t size)
{
    const unsigned char       *p = (const unsigned char *)data;
    const unsigned char *const q = (const unsigned char *)data + size;
    ssize_t                    n;

    if (descriptor == -1) {
        errno = EBADF;
        return 0;
    }

    while (p < q) {

        n = write(descriptor, p, (size_t)(q - p));
        if (n > 0)
            p += n;
        else
        if (n != -1) {
            errno = EIO;
            return (size_t)(p - (const unsigned char *)data);
        } else
            return (size_t)(p - (const unsigned char *)data);
    }

    errno = 0;
    return (size_t)(p - (const unsigned char *)data);
}

在这种情况下,写入的数据量总是returned。此版本还在所有情况下设置 errno - 通常 errno 不设置,除非在错误情况下。

尽管这意味着如果中途发生错误,并且函数将 return 成功写入的数据量(使用之前的 write() 调用),始终设置 return 的原因=19=]是为了更容易检测错误,本质上是将状态(errno)与写入计数分开。


有时,我需要一个将调试消息从信号处理程序写入标准错误的函数。 (标准 <stdio.h> I/O 不是 async-signal 安全的,因此在任何情况下都需要一个特殊的功能。)我希望该功能即使在信号传递时也能中止——这没什么大不了的写入失败,只要它不影响程序的其余部分——但保持 errno 不变。这将专门打印字符串,因为它是预期的用例。请注意 strlen() 不 async-signal 安全,因此使用显式循环代替。

int stderr_note(const char *message)
{
    int retval = 0;

    if (message && *message) {
        int         saved_errno;
        const char *ends = message;
        ssize_t     n;

        saved_errno = errno;
        while (*ends)
            ends++;

        while (message < ends) {
            n = write(STDERR_FILENO, message, (size_t)(ends - message));
            if (n > 0)
                message += n;
            else {
                if (n == -1)
                    retval = errno;
                else
                    retval = EIO;
                break;
            }
        }

        if (!retval && message != ends)
            retval = EIO;

        errno = saved_errno;
    }

    return retval;
}

如果消息成功写入标准输出,则此版本 returns 为 0,否则为非零错误代码。如前所述,它始终保持 errno 不变,以避免在信号处理程序中使用时在主程序中产生意外的副作用。


在处理意外错误或来自系统调用的 return 值时,我使用非常简单的原则。主要原则是永远不要悄悄丢弃或破坏用户数据。如果数据丢失或损坏,程序应始终通知用户。一切意外都应视为错误。

程序中只有部分写入涉及用户数据。许多是信息性的,例如使用信息或进度报告。对于那些,我宁愿忽略意外情况,或者完全跳过该写入。这取决于写入数据的性质。

总而言之,我不关心标准对return值的看法:我处理所有这些。对每种(类型)结果的响应取决于正在写入的数据——具体来说,是该数据对用户的重要性。因此,即使在单个程序中,我也会使用多个不同的实现。