我想要实现一个正确的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
。 还有很多其他的情况下,没有什么可写的,但总的来说,他们都有特定的错误代码与他们相关联。
该文件本身将是从命令行上给出的path/名称打开的。 所以它通常是一个普通的文件,但是用户当然可以通过pipe道,inputredirect,通过特殊的设备如/dev/stdout
等等。 我至less在open
调用控制和O_NONBLOCK
标志不传递打开。 我无法合理地检查所有文件系统的行为,所有的特殊设备(即使我可以,更多将被添加),所以我想知道如何处理这个以合理和一般的方式 。
* …为非零缓冲区大小。
除非你试图调用未指定的行为,否则你不会从write()
得到一个零结果,除非你试图写零字节(问题中的代码避免这样做)。
write()
的POSIX规范涵盖了这个问题,我相信。
write()函数将尝试从buf指向的缓冲区写入nbyte个字节到与打开的文件描述符fildes关联的文件。
在执行下面描述的任何操作之前,如果nbyte为零且文件是常规文件,则write()函数可能会检测并返回错误,如下所述。 在没有错误的情况下,或者如果没有执行错误检测, write()函数将返回零并且没有其他结果。 如果nbyte为零,并且该文件不是常规文件,则结果是未指定的。
这表明,如果您请求写入零字节,您可能会得到一个返回值为零,但有一个警告 – 它必须是一个常规文件,并且如果检测到错误,如EBADF
可能会出错;没有指定如果文件描述符没有引用常规文件会发生什么情况。
如果write()请求写入的字节多于(例如,[XSI] [Option Start]文件大小限制的进程或[Option End]介质的物理端口)的空间,应该写入有空间的字节。 例如,假设在达到限制之前,文件中有20个字节的空间。 写入512字节将返回20.下一次写入非零字节数将导致失败返回(除非如下所述)。
[XSI]⌦如果请求将导致文件大小超过进程的软文件大小限制,并且没有空间写入任何字节,则请求将失败,实现应为该线程生成SIGXFSZ信号。 ⌫
如果write()在写入任何数据之前被一个信号中断,则它将返回-1,并将errno设置为[EINTR]。
如果write()在成功写入一些数据后被一个信号中断,它将返回写入的字节数。
如果nbyte的值大于{SSIZE_MAX},则结果是实现定义的。
这些规则并没有真正授予返回0的权限(尽管nbyte
可能会说nbyte
的值太大,可能会被定义为返回0)。
当试图写入支持非阻塞写入的文件描述符(非管道或FIFO)并且不能立即接受数据时:
如果O_NONBLOCK标志清除, write()将阻塞调用线程,直到数据被接受。
如果设置了O_NONBLOCK标志,则write()不应该阻塞该线程。 如果一些数据可以在不阻塞线程的情况下写入 , write()应该写入它所能写入的数据并返回写入的字节数。 否则,它将返回-1并将errno设置为[EAGAIN]。
…晦涩的文件类型的详细信息 – 其中一些未指定的行为…
返回值
成功完成后,这些函数将返回实际写入与fildes相关文件的字节数。 这个数字永远不会大于字节 。 否则,返回-1,并设置errno来指示错误。
所以,因为你的代码避免了试图写零字节,只要len
不大于{SSIZE_MAX},并且只要你没有写文件类型(比如一个共享内存对象或者一个类型化的内存对象)不应该看到由write()
返回的零。
稍后在用于write()
的POSIX页面中,在基本原理部分中有以下信息:
如果POSIX.1-2008这个卷需要返回
-1
,并且errno
设置为[EAGAIN],那么大多数历史实现将返回零(O_NDELAY
标志被设置,这是O_NONBLOCK
的历史前驱,但是它本身并不在这个卷中POSIX.1-2008)。 本卷的POSIX.1-2008中的错误指示被选择,以便应用程序可以区分这些情况与文件结束。 虽然write()
不能接收到文件结束的指示,但是read()
可以,而且这两个函数具有相似的返回值。 而且,一些现有的系统(例如,第八版)允许写入零字节意味着读者应该得到文件结束指示; 对于这些系统,来自write()
的返回值为零表示成功写入文件结束指示。
因此,尽管POSIX(大部分如果不是全部的话)排除了从write()
返回零的可能性,但是在相关系统上有write()
返回为零的现有技术。
这取决于文件描述符引用的内容。 当你在一个文件描述符上调用write
时,内核最终会在关联的文件操作向量中调用写例程,该向量对应于文件描述符引用的底层文件系统或设备。
大多数正常的文件系统永远不会返回0,但设备可能会做任何事情。 您需要查看相关设备的文档以了解其可能的功能。 设备驱动程序返回写入的0字节是合法的(内核不会将其标记为错误或任何内容),如果是,写入系统调用将返回0。
Posix为支持非阻塞操作的管道,FIFO和nbyte
定义它,在nbyte
(第三个参数)为正值且呼叫未中断的情况下:
如果O_NONBLOCK清零,它将返回
nbyte
。
换句话说,除非nbyte
为零,否则它不能返回0,在提到的情况下,它也不能返回一个短的长度。
我认为,唯一可行的办法(除了完全忽略这个问题,似乎是根据文件做的事情)是允许 “就地旋转”。
你可以实现一个重试计数,但是如果这个非常不可能的 “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()
返回0或-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而被送进监狱,所以即使你的聪明代码与标准所说的完全一致,也不能保证事情总能起作用。 有时候最好是把事情做好,当他们破产的时候依靠好的公司。 如果正常的文件写入开始回头,你会成为一个很好的公司,很有可能在你的任何用户注意到之前很久就会得到修复。
NB大约20年前,我从事文件系统的实现工作,我们试图成为标准律师关于其中一个操作的行为(不是write
,但同样的原则适用)。 我们的“按照这个顺序返回数据是合法的”,被大量的错误报告破坏了,这些应用程序以某种方式期待着事情,最后只是修复它而不是去争取同样的战斗每个错误报告。 对于任何人来说,当时(可能还有今天)的许多事情都会被期待回来.
并且..
作为目录中的前两个条目(至少当时)不是由任何标准强制的。