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

时间:2017-02-24 15:29:14

标签: c multithreading unix solaris solaris-10

假设我已将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周围添加自己的同步原语?

2 个答案:

答案 0 :(得分:5)

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()。具体来说,它说

  

如果两个线程分别调用其中一个函数,则每个调用应该看到另一个调用的所有指定效果,或者没有一个。

如果你的写入被定向到一个常规文件,那么这就足以断定你提出的多线程动作是安全的,因为它们不会相互干扰,并且一次调用写入的数据不会与任何线程中的不同调用所写的内容混合在一起。遗憾的是,/dev/poll不是常规文件,因此不适用于您。

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

答案 1 :(得分:4)

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

  

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

     

...

     

返回值

     

成功完成后,这些函数将返回实际写入的字节数 ...

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

在实践中,如果您只进行相对较小的write()来电,则可能永远不会看到部分write(),其中"小"和"可能"是不确定的值取决于您的实现。

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

但是,在这种情况下,你会写下"一小撮内核结构的字节。您可以浏览Illumos.org上的Solaris /dev/poll内核驱动程序源代码,看看部分write()的可能性。我怀疑这几乎是不可能的 - 因为我回过头看了十年前我为公司的软件库编写的多平台民意调查课程。在Solaris上,它使用来自多个线程的/dev/poll和解锁write()调用。它已经工作了十年......

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

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

dpwrite()函数是/dev/poll驱动程序中实际执行" write"操作。我使用引号是因为它根本不是写操作 - 数据不会被转移,因为内核中的数据代表了被轮询的文件描述符集。

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

在此之后,最后一次回叫是在最后 - 如果没有错误,整个呼叫都标记为成功,或整个呼叫在任何错误上失败:

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最终成为调用成功后write()返回的值。

所以电话肯定似乎是全有或全无。虽然在传入多个描述符时成功处理早期描述符后,代码似乎有办法在文件描述符上返回错误,但代码似乎没有返回任何部分成功指示。

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