c-Can-write(2)返回写入的0字节*,如果返回,该怎么办



我想实现一个合适的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;
} 

但这提出了CCD_ 3是否可以有效地返回写入的0字节以及在这种情况下该怎么做的问题。如果这种情况持续存在,上面的代码将对write调用进行热旋转,这似乎是个坏主意。只要返回了而不是零的东西,你就在向前迈进。

write的手册页有点模棱两可。例如,上面写着:

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

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

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

上面避免了这种情况,因为我从不使用len == 0调用write。还有很多其他情况下什么都写不出来,但总的来说,它们都有特定的错误代码。

文件本身将是来自命令行上给定的路径/名称的opened。因此,它通常是一个常规文件,但用户当然可以传递管道、进行输入重定向、传递/dev/stdout等特殊设备。我至少可以控制open调用,而O_NONBLOCK标志不会传递到打开状态。我无法合理地检查所有文件系统、所有特殊设备的行为(即使可以,也会添加更多),所以我想知道如何以合理和通用的方式处理这一问题


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

TL;DR摘要

除非您特意调用未指定的行为,否则您不会从write()返回零结果,除非您尝试写入零字节(问题中的代码避免这样做)。

POSIX表示:

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

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

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

这表明,如果您请求写入零字节,您可能会得到零的返回值,但有一系列注意事项——它必须是一个常规文件,如果检测到类似EBADF的错误,您可能就会得到错误,并且未指定如果文件描述符不引用常规文件会发生什么。

如果write。例如,假设在达到限制之前,文件中还有20个字节的空间。512字节的写入将返回20。下一次写入非零字节数将返回失败(以下说明除外)。

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

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

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

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

这些规则并没有真正允许返回0(尽管学究可能会说nbyte的值太大,可能会被定义为无法返回0)。

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

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

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

…模糊文件类型的详细信息——其中一些具有未指定行为…

返回值

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

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


POSIX基本原理说:

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

在POSIX.1-2008的这个卷需要返回CCD_ 19并且CCD_,大多数历史实现返回零(设置了O_NDELAY标志,它是O_NONBLOCK的历史前身,但它本身不在POSIX.1-2008的本卷中)。选择POSIX.1-08的本卷的错误指示是为了让应用程序能够将这些情况与文件结尾区分开。当write()不能接收到文件结束的指示时,read()可以,并且这两个函数具有相似的返回值。此外,一些现有的系统(例如,第八版)允许写入零字节,这意味着读取器应该得到文件结尾指示;对于那些系统,来自write()的返回值为零指示文件结束指示的成功写入。

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

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

大多数正常的文件系统永远不会返回0,但设备可能会执行任何操作。你需要查看相关设备的文档,看看它可能会做什么。设备驱动程序返回0个写入的字节是合法的(内核不会将其标记为错误或任何事情),如果返回,写入系统调用将返回0。

Posix为支持非阻塞操作的管道、FIFO和FD定义了它,在nbyte(第三个参数)为正并且调用没有中断的情况下:

如果O_NONBLOCK清除。。。则返回CCD_ 30。

换句话说,在上述情况下,它不仅不能返回0,除非write()1为零,而且也不能返回短长度。

我认为唯一可行的方法(除了完全忽略问题之外,根据文档,这似乎是应该做的事情)是允许"原地旋转"。

您可以实现重试计数,但如果这种极不可能"长度为非零的0返回"是由于某些瞬态情况造成的,则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;
}

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

下面是三个示例,它们都希望在块描述符上工作。(也就是说,他们认为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,否则返回非零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);
}

在这种情况下,总是返回写入的数据量。此版本还在所有情况下设置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不变,以避免在信号处理程序中使用主程序时出现意外的副作用。


在处理系统调用的意外错误或返回值时,我使用非常简单的原则。主要原则是永远不要默默地丢弃或篡改用户数据。如果数据丢失或损坏,程序应始终通知用户。所有出乎意料的事情都应被视为错误。

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

总之,我不在乎标准对返回值的描述:我处理所有返回值。对每个(类型)结果的响应取决于正在写入的数据——特别是该数据对用户的重要性。正因为如此,即使在一个程序中,我也会使用几种不同的实现。

我认为整个问题没有必要。你只是太小心了。您希望该文件是一个常规文件,而不是套接字、设备、fifo等。我想说,从write到不等于len的常规文件的任何返回都是不可恢复的错误。不要试图修复它。你可能已经填满了文件系统,或者你的磁盘坏了,或者诸如此类的事情。(这一切都假设你没有配置信号来中断系统调用)

对于常规文件,我不知道有哪个内核没有进行所有必要的重试来写入数据,如果失败,错误很可能严重到应用程序无法修复。如果用户决定将非常规文件作为参数传递,那又怎样?这是他们的问题。他们的脚和枪,让他们开枪。

通过尝试在代码中修复此问题,您更有可能通过创建无休止的循环消耗CPU、填充文件系统日志或挂起来使情况变得更糟。

不要处理0或其他短写入,只需在除len之外的任何返回上打印一个错误即可退出。一旦你从一个用户那里得到了一个正确的错误报告,而这个用户实际上有写操作失败的正当理由,那么就修复它。这很可能永远不会发生,因为这几乎是每个人都会做的事情。

是的,有时阅读POSIX并找到边缘案例并编写代码来处理它们是很有趣的。但是操作系统开发人员不会因为违反POSIX而入狱,所以即使你的聪明代码与标准所说的完全匹配,也不能保证一切都会正常。有时候,最好只是把事情做好,并在事情破裂时依靠良好的陪伴。如果常规文件写入开始返回不足,那么您所在的公司非常好,很可能在任何用户注意到之前很久就已经修复了。

注意:大约20年前,我从事文件系统实现工作,我们试图成为其中一个操作行为的标准律师(不是write,但适用相同的原则)。我们的"按此顺序返回数据是合法的"被大量关于坏应用程序的错误报告所压制,这些错误报告以某种方式预期会发生什么,最终,只修复它会更快,而不是在每个错误报告中进行同样的斗争。对于任何想知道的人来说,当时(可能仍然是今天)很多事情都期望readdir返回...作为目录中的前两个条目,而目录(至少在当时)没有任何标准强制要求。