【问题标题】:Is writev() really atomic?writev() 真的是原子的吗?
【发布时间】:2019-04-05 02:09:16
【问题描述】:

man writev 是这样说的:

readv() 和 writev() 执行的数据传输是原子的:writev() 写入的数据作为单个块写入,不会与其他进程的写入输出混合(但请参见 pipe(7)例外);类似地,readv() 是有保证的

这是来自man 7 pipe

   O_NONBLOCK disabled, n <= PIPE_BUF
          All n bytes are written atomically; write(2) may block if there is not room for n bytes to be written immediately

   O_NONBLOCK enabled, n <= PIPE_BUF
          If there is room to write n bytes to the pipe, then write(2) succeeds immediately, writing all n bytes; otherwise write(2) fails, with errno set to EAGAIN.

   O_NONBLOCK disabled, n > PIPE_BUF
          The write is nonatomic: the data given to write(2) may be interleaved with write(2)s by other process; the write(2) blocks until n bytes have been written.

   O_NONBLOCK enabled, n > PIPE_BUF
          If  the pipe is full, then write(2) fails, with errno set to EAGAIN.  Otherwise, from 1 to n bytes may be written (i.e., a "partial write" may occur; the caller should check the return value from write(2) to see how many bytes were actually written), and these bytes may be interleaved with writes by other processes.
$ cat writev.c
#include <string.h>
#include <sys/uio.h>

int
main(int argc,char **argv) {
    static char part1[] = "ST";
    static char part2[] = "\n";
    struct iovec iov[2];

    iov[0].iov_base = part1;
    iov[0].iov_len = strlen(part1);

    iov[1].iov_base = part2;
    iov[1].iov_len = strlen(part2);

    writev(1,iov,2);

    return 0;
}
$ gcc writev.c
$ unbuffer bash -c 'for ((i=0; i<50; i++)); do ./a.out & ./a.out; done' | wc -c
300  # < PIPE_BUF

# Run the following several times to get the output corrupted
$ unbuffer bash -c 'for ((i=0; i<50; i++)); do ./a.out & ./a.out; done' | sort | uniq -c
      4 
     92 ST
      4 STST

如果 writev 是原子的(根据文档),谁能解释为什么不同写入的输出是交错的?

更新:

来自strace -fo /tmp/log unbuffer bash -c 'for ((i=0; i&lt;10000; i++)); do ./a.out &amp; ./a.out; done' | sort | uniq -c的一些相关数据

13301 writev(1, [{iov_base="ST", iov_len=2}, {iov_base="\n", iov_len=1}], 2 <unfinished ...>
13302 mprotect(0x56397d7d8000, 4096, PROT_READ) = 0
13302 mprotect(0x7f7190c68000, 4096, PROT_READ) = 0
13302 munmap(0x7f7190c51000, 90695)     = 0
13302 writev(1, [{iov_base="ST", iov_len=2}, {iov_base="\n", iov_len=1}], 2) = 3
13301 <... writev resumed> )            = 3
24814 <... select resumed> )            = 1 (in [4])
13302 exit_group(0 <unfinished ...>
13301 exit_group(0 <unfinished ...>
13302 <... exit_group resumed>)         = ?
13301 <... exit_group resumed>)         = ?
24814 futex(0x55b5b8c11cc4, FUTEX_WAKE_PRIVATE, 2147483647 <unfinished ...>
24807 <... futex resumed> )             = 0
24814 <... futex resumed> )             = 1
24807 futex(0x7f7f55e8f920, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...>
13302 +++ exited with 0 +++
24807 <... futex resumed> )             = -1 EAGAIN (Resource temporarily unavailable)
13301 +++ exited with 0 +++
24807 futex(0x7f7f55e8f920, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
24814 futex(0x7f7f55e8f920, FUTEX_WAKE_PRIVATE, 1 <unfinished ...>
24807 <... futex resumed> )             = 0
24814 <... futex resumed> )             = 0
24807 read(4,  <unfinished ...>
24814 select(6, [5], [], [], NULL <unfinished ...>
24807 <... read resumed> "STST\n\n", 4096) = 6
24808 <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 13302
24807 write(1, "STST\n\n", 6 <unfinished ...>

【问题讨论】:

  • 如果您不使用stdio,则无需使用unbuffer
  • 我在没有 unbuffer 的情况下尝试过:for ((i=0; i&lt;50; i++)); do ./a.out &amp; ./a.out; done | sort | uniq -c 并在 Debian 3.2.89-2 上获得了 100 ST
  • 这肯定是由unbuffer 正在做的事情引起的。
  • 您要求它在命令中写入 2 个缓冲区。我认为,单个缓冲区的写入是原子的。但是不同的缓冲区可以交错。你的输出支持这个理论。在他的文档中也暗示了这一点。
  • @user207421:程序并没有真正写入管道。使用unbuffer,程序a.out 的fd 1 进入伪tty,并且有一个辅助进程收集写入pty 的数据并将其写入管道。这个想法是,这将鼓励使用 stdio 到行缓冲区的程序。因此,终端似乎是该规则的另一个(未记录的)例外。

标签: c++ c linux-kernel system-calls


【解决方案1】:

如指定,对于管道是,当总 iov 长度不超过 PIPE_BUF 时,因为:

writev() 函数应等价于 write(),但如下所述

管道没有例外(管道这个词甚至没有出现在the writev specification中)。

在 Linux 的实践中,也许不是。 writev 等效于单个 write 仅适用于实现“新”(大约 15 年前)基于 iov 的读/写后端的内核文件类型。有些,如终端,只实现使用单个缓冲区的旧接口,Linux 将writev(或readv)模拟为多个write 调用(或相应的read 调用)。 readv 的情况也有问题,你可以看到in this commit to musl libc

我不确定管道是否受到此问题的影响。您必须深入研究内核源代码。

【讨论】:

  • 终端正是这里的重点,因为这就是unbuffer 所做的;它使程序的标准输出 fd 转到伪终端(然后由辅助进程将其写入“原始”标准输出)。但是a.out 这里实际上并没有写入管道,所以关于管道的部分无关紧要。
  • @NateEldredge:unbuffer 是什么?一个用 stdout 包装执行的程序通过一个 pty 来欺骗程序使用行缓冲?如果是这样,那么是的,我引用的 Linux tty 错误将准确解释 OP 所看到的。
  • 是的,就是这样。它是Expect 的一部分。
  • @NateEldredge,你是对的,输出不是管道而是 pty。无法将评论标记为答案。
猜你喜欢
  • 1970-01-01
  • 2017-06-13
  • 2020-08-06
  • 2013-10-11
  • 2011-03-03
  • 2010-12-11
  • 2020-05-07
  • 2013-01-26
  • 2019-07-03
相关资源
最近更新 更多