可写(2)返回0字节写*,如果有,该怎么办?

时间:2017-01-27 22:42:55

标签: c linux file-io posix

我想实现一个正确的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()是否可以有效地返回写入的0字节以及在这种情况下该怎么做。如果情况仍然存在,上面的代码只会在write电话上热播,这似乎是一个坏主意。只要返回其他而不是零的东西,你就会取得进展。

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

  

成功时,返回写入的字节数(零表示   什么都写不出来。)

这似乎表明在某些情况下可行。只明确调出了一个这样的场景:

  

如果count为零且fd引用常规文件,则write()可以   如果检测到以下错误之一,则返回失败状态。如果不   检测到错误,或者不执行错误检测,0将是   返回时没有造成任何其他影响。如果count为零且fd   指的是普通文件以外的文件,结果不是   指定。

上面避免了这种情况,因为我从未使用write致电len == 0。还有很多其他情况下无法写入任何内容,但一般来说它们都有与之关联的特定错误代码。

文件本身将是命令行中给出的路径/名称的open。所以通常是常规文件,但用户当然可以传递管道,输入重定向,传递/dev/stdout等特殊设备等内容。我至少控制了open调用,并且O_NONBLOCK标志未传递给open。我无法合理地检查所有文件系统的行为,所有特殊设备(即使我可以,还会添加更多),所以我想知道如何以合理和一般的方式处理这个问题< /强>

* ...表示非零缓冲区大小。

6 个答案:

答案 0 :(得分:7)

TL; DR摘要

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

POSIX说:

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

  

write()函数应尝试将 nbyte 字节从 buf 指向的缓冲区写入与打开文件关联的文件描述符, fildes

     

在执行下面描述的任何操作之前,如果 nbyte 为零且文件是常规文件, write()函数可能会检测并返回错误,如上所述下面。如果没有错误,或者未执行错误检测, write()函数将返回零并且没有其他结果。如果 nbyte 为零且文件不是常规文件,则结果未指定。

这表明如果你请求写入零字节,你可能会得到零返回值,但是有一堆警告 - 它必须是常规文件,如果像{{{}这样的错误你可能会收到错误检测到1}},并且未指定如果文件描述符不引用常规文件会发生什么。

  

如果 write()请求写入更多字节而不是有空间(例如,[XSI] [Option Start]进程的文件大小限制或[Option End]媒体的物理结束),只有有空间的字节才能写入。例如,假设在达到限制之前文件中有20个字节的空间。写入512个字节将返回20.下一次写入非零字节数将导致失败返回(除非如下所述)。

     

[XSI]⌦如果请求会导致文件大小超过进程的软文件大小限制,并且没有空间可以写入任何字节,则请求将失败并且实现将生成SIGXFSZ信号线程。 ⌫

     

如果 write()在写入任何数据之前被信号中断,它将返回-1并将errno设置为[EINTR]。

     

如果 write()在成功写入一些数据后被信号中断,它将返回写入的字节数。

     

如果 nbyte 的值大于{SSIZE_MAX},则结果是实现定义的。

这些规则并不真正授予返回0的权限(尽管学究者可能会说EBADF的值过大可能会被定义为返回0)。

  

尝试写入支持非阻塞写入且不能立即接受数据的文件描述符(管道或FIFO除外)时:

     
      
  • 如果O_NONBLOCK标志清除, write()将阻止调用线程,直到可以接受数据。

  •   
  • 如果设置了O_NONBLOCK标志,则 write()不会阻塞该线程。如果可以在不阻塞线程的情况下写入某些数据, write()应该写入它可以写入的字节数。否则,它将返回-1并将 errno 设置为[EAGAIN]。

  •   
     

...晦涩的文件类型的详细信息 - 其中一些具有未指定的行为......

     

返回值

     

成功完成后,这些函数将返回实际写入与 fildes 相关联的文件的字节数。此数字不得大于 byte 。否则,将返回-1并设置 errno 以指示错误。

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

POSIX Rationale说:

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

  

如果此POSIX.1-2008卷需要返回write()并且-1设置为[EAGAIN],则大多数历史实现返回零(设置errno标志,是O_NDELAY的历史前身,但本身不是POSIX.1-2008中的。选择此卷POSIX.1-2008中的错误指示,以便应用程序可以将这些情况与文件结尾区分开来。虽然O_NONBLOCK无法接收文件结尾的指示,write()可以,并且这两个函数具有相似的返回值。此外,一些现有系统(例如,第八版)允许写入零字节以表示读者应获得文件结束指示;对于那些系统,从read()返回零值表示成功写入文件结束指示。

因此,虽然POSIX(很大程度上,如果不是全部)排除了write()归零的可能性,但相关系统上的现有技术确实write()返回零。

答案 1 :(得分:6)

取决于文件描述符引用的内容。当您在文件描述符上调用write时,内核最终会在关联的文件操作向量中调用写例程,该向量对应于文件描述符引用的基础文件系统或设备。

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

答案 2 :(得分:3)

Posix为支持非阻塞操作的管道,FIFO和FD定义它,如果nbyte(第三个参数)为正且呼叫未被中断:

  

如果O_NONBLOCK被清除......它将返回nbyte

换句话说,除非nbyte为零,否则不能返回0,在上述情况下,它也不能返回短的长度。

答案 3 :(得分:1)

我认为唯一可行的方法(除了完全忽略问题,根据文档似乎要做的事情)是允许&#34;旋转到位&#34;。

您可以实施重试计数,但如果极不可能&#34; 0返回非零长度&#34;是由于一些短暂的情况 - LapLink队列可能已满;我记得那个驱动程序做了奇怪的事情 - 循环可能会如此之快,以至于任何合理的重试计数都会被淹没;如果你有其他设备,而不是一个不可忽略的时间返回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;
}

答案 4 :(得分:1)

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

下面是三个例子,它们都希望能够处理阻塞描述符。 (也就是说,他们认为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()返回零或除-1以外的负值,则会中止。

因为上面没有理由返回部分写入计数,所以如果成功则返回0,否则返回非零错误代码。

写入重要数据时,如果传送信号会中断写入,接口会有所不同:

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);
}

在这种情况下,始终返回写入的数据量。此版本在所有情况下都会设置errno - 除非出现错误,否则通常不会设置errno

虽然这意味着如果错误发生在中途,并且该函数将返回成功写入的数据量(使用之前的write()次调用),则始终设置errno的原因是使错误检测更容易,主要是将状态(errno)与写入计数分开。

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

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;
}

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

在处理意外错误或从系统调用返回值时,我使用非常简单的原则。主要原则是永远不会默默地丢弃或破坏用户数据。如果数据丢失或损坏,程序应始终通知用户。意想不到的一切都应该被视为错误。

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

总之,我不关心标准对返回值的看法:我处理它们。对每种(类型)结果的响应取决于正在写入的数据 - 具体而言,该数据对用户的重要性。因此,即使在单个程序中,我也会使用几种不同的实现。

答案 5 :(得分:0)

我想说整个问题是不必要的。你只是太小心了。您希望该文件是常规文件,而不是套接字,不是设备,不是fifo等。我会说从write到不等于{的常规文件的任何返回{1}}是一个无法恢复的错误。不要试图解决它。你可能填满了文件系统,或者你的磁盘坏了,或类似的东西。 (这都假定您没有配置信号来中断系统调用)

对于常规文件,我不知道任何内核尚未进行所有必要的重试以获取您的数据,如果失败,则错误很可能非常严重,超出了应用程序的范围。修理它。如果用户决定传递一个非常规文件作为参数,那么呢?这是他们的问题。他们的脚和枪,让他们射击。

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

不要处理0或其他短写,只需在len以外的任何回复上打印错误并退出。一旦您从用户那里获得了一个实际上有正确写入失败原因的错误报告,那么请先修复它。很可能这种情况永远不会发生,因为这几乎是每个人所做的事情。

是的,有时阅读POSIX并找到边缘案例并编写代码来处理它们很有趣。但操作系统开发人员因违反POSIX而无法入狱,因此即使您的聪明代码完全符合标准所说的内容,也无法保证事情始终有效。有时候,做完事情并依赖于在他们休息时保持良好的公司状态会更好。如果常规文件写入开始缩短,那么您将成为一个如此优秀的公司,很可能在您的任何用户注意到之前很久就会修复它。

N.B。大约20年前,我参与了一个文件系统实现,我们试图成为标准律师关于其中一个操作的行为(不是len,但同样的原则适用)。我们&#34;以此顺序返回数据是合法的。被破坏的应用程序的臭虫报告大量沉默,这些应用程序以某种方式预期事情,最后只是更快地修复它而不是在每个错误报告中打击相同的战斗。对于任何有奇迹的人来说,当时很多事情(可能仍然是今天)期望write返回readdir.作为目录中的前两个条目(至少在那时)没有任何标准强制要求。