同时从多个线程调用 write() 是否安全?

Is write() safe to be called from multiple threads simultaneously?

假设我已将 dev/poll 打开为 mDevPoll,这样调用代码对我来说安全吗

struct pollfd tmp_pfd;
tmp_pfd.fd = fd;
tmp_pfd.events = POLLIN;

// Write pollfd to /dev/poll
write(mDevPoll, &tmp_pfd, sizeof(struct pollfd));

...同时来自多个线程,还是我需要在 mDevPoll 周围添加自己的同步原语?

Solaris 10 声称 POSIX 兼容。 write()函数不在the handful of system interfaces that POSIX permits to be non-thread-safe中,所以我们可以得出结论,在Solaris 10上,从一般意义上来说,从两个或多个线程同时调用write()是安全的。

POSIX 在对常规文件或符号链接进行操作时,也会在 functions whose effects are atomic relative to each other 中指定 write()。具体来说,它说

If two threads each call one of these functions, each call shall either see all of the specified effects of the other call, or none of them.

如果您的写入是定向到一个常规文件,那么就足以得出您建议的多线程操作是安全的结论,因为它们不会相互干扰,并且不会在一次调用中写入数据不会与任何线程中的不同调用所编写的内容混合在一起。不幸的是,/dev/poll 不是常规文件,因此这并不直接适用于您。

您还应注意,通常不需要 write() 来传输单个调用中指定的全部字节数。因此,对于一般用途,必须准备好通过使用循环在多个调用中传输所需的字节。 Solaris 可能会提供超出 POSIX 所表达的适用保证,可能特定于目标设备,但如果没有此类保证,可以想象您的一个线程执行了部分写入,而下一次写入由另一个线程执行。这很可能不会产生您想要或期望的结果。

理论上不安全,尽管 write() 是完全线程安全的(除了实现错误...)。 Per the POSIX write() standard(强调我的): .

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.

...

RETURN VALUE

Upon successful completion, these functions shall return the number of bytes actually written ...

无法保证您不会获得部分 write(),因此即使每个单独的 write() 调用都是原子的,也不一定 完整,因此您仍然可以获得交错数据,因为可能需要多次调用 write() 才能完全写入所有数据。

实际上,如果您只进行相对较小的 write() 调用,您可能永远不会看到部分 write(),其中 "small" 和 "likely" 是不确定的值取决于您的实施。

我经常交付使用未锁定的单个 write() 调用对使用 O_APPEND 打开的常规文件进行调用的代码,以提高日志记录的性能 - 然后构建一个日志条目 write()一次调用整个条目。在 Linux 和 Solaris 系统上,我 从未 看到部分或交错的 write() 结果,即使许多进程写入同一个日志文件。但话又说回来,它是一个文本日志文件,如果确实发生了部分或交错 write(),则不会造成真正的损坏,甚至不会丢失数据。

不过,在这种情况下,您是 "writing" 内核结构的几个字节。您可以在 Illumos.org 浏览 Solaris /dev/poll 内核驱动程序源代码,看看部分 write() 的可能性有多大。我怀疑这实际上是不可能的——因为我刚刚回去查看了十年前我为公司的软件库编写的多平台民意调查 class。在 Solaris 上,它使用来自多个线程的 /dev/poll 和未锁定的 write() 调用。十年来一直运行良好...

Solaris /dev/pool设备驱动源码分析

可在此处找到 (Open)Solaris 源代码:http://src.illumos.org/source/xref/illumos-gate/usr/src/uts/common/io/devpoll.c#628

dpwrite()函数是/dev/poll驱动程序中实际执行"write"操作的代码。我使用引号是因为它根本不是真正的写操作 - 数据传输不如内核中表示正在轮询的文件描述符集的数据更新那么多。

数据从用户 space 复制到内核 space - 到使用 kmem_alloc() 获得的内存缓冲区。我看不出有任何可能的方式可以成为部分副本。分配成功或不成功。代码在执行任何操作之前可能会被中断,因为它等待对内核结构的独占 write() 访问。

之后,最后一个 return 调用结束 - 如果没有错误,则整个调用被标记为成功,或者整个调用因任何错误而失败:

995     if (error == 0) {
996     /*
997      * The state of uio_resid is updated only after the pollcache
998      * is successfully modified.
999      */
1000        uioskip(uiop, copysize);
1001    }
1002    return (error);
1003}

如果你深入了解 Solaris 内核代码,你会发现 uio_resid 是 return 在成功调用后由 write() 编辑的值。

所以这个电话肯定看起来是全有或全无。当传入多个描述符时,在成功处理较早的描述符后,代码似乎有办法 return 文件描述符出错,但代码似乎没有 return 任何部分成功指示.

如果你一次只处理一个文件描述符,我会说 /dev/poll write() 操作是完全线程安全的,而且它几乎肯定是线程安全的 "writing" 更新多个文件描述符,因为驱动程序没有明显的方法 return 部分 write() 结果。