当select报告文件描述符可写时,对于非阻塞文件描述符的write()函数是否可能返回EAGAIN?

3

我正在尝试追踪OS X(10.8.2)中的一些奇怪行为。基本上,我正在打开一个管道,并填充数据直到它无法写入。然而,我发现,根据我尝试写入的块的大小,有时即使select声称该管道仍然可写,我仍会从write()调用中得到EAGAIN。这是一些测试代码:

#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/select.h>

#define START 1
#define END 16

int is_writeable(int fd) {
    struct timeval timeout;
    timeout.tv_sec = 0;
    timeout.tv_usec = 0;

    fd_set ws;

    FD_ZERO(&ws);
    FD_SET(fd, &ws);

    if(select(fd+1, NULL, &ws, NULL, &timeout) < 0) {
        return -1;
    }

    if(FD_ISSET(fd, &ws))
        return 1;
    else
        return 0;
}

int main(int argc, char *argv[]) {

    int pipes[2];
    int rp, wp, i, errval, fails, tmp;

    char testbuf[END];
    char destbuf[128000];

    for(i = START; i < END; i++) {
        int byte_count = 0;
        printf("%i: ", i);
        fails = 0;

        pipes[0] = 0;
        pipes[1] = 0;

        if(pipe(pipes) < 0) {
            printf("PIPE FAIL\n");
            break;
        }
        rp = pipes[0];
        wp = pipes[1];

        int flags = fcntl(wp, F_GETFL, 0);
        if(fcntl(wp, F_SETFL, flags | O_NONBLOCK) == -1) {
            fails = 4;
        }

        if(is_writeable(wp) != 1) {
            fails = 1;
        }

        while(!fails) {
            // printf(".");
            if(is_writeable(wp) < 1) {
                break;
            }

            tmp = write(wp, testbuf, i);
            //No bytes written, fail
            if(tmp < 0) {
                if(errno == EAGAIN) {
                    if(is_writeable(wp) == 1) {
                        fails = 3;
                        break;
                    }
                } else {
                    fails = 2;
                    perror("During write");

                    break;
                }

            } else {
                byte_count += tmp;
            }
            //Errno is eagain, fail
        }
        printf("byte count %i, ", byte_count);

        if(fails)
            printf("FAIL, %i\n", fails);
        else
            printf("PASS\n");

        if(close(wp) != 0)
            printf("WP CLOSE FAIL\n");
        if(close(rp) != 0)
            printf("RP CLOSE FAIL\n");
    }

}

以下是输出结果:

1: byte count 16384, PASS
2: byte count 16384, PASS
3: byte count 65535, FAIL 3
4: byte count 16384, PASS
5: byte count 65535, FAIL 3
6: byte count 65532, FAIL 3
7: byte count 65534, FAIL 3
8: byte count 16384, PASS
9: byte count 65529, FAIL 3
10: byte count 65530, FAIL 3
11: byte count 65527, FAIL 3
12: byte count 65532, FAIL 3
13: byte count 65533, FAIL 3
14: byte count 65534, FAIL 3
15: byte count 65535, FAIL 3

请注意,失败情况3是指write()调用返回-1,但select仍然报告文件句柄可写。下面是一个在Ruby中展示相同失败的简短示例:
(1..10).each do |i|
  passes = true
  begin
    (rp, wp) = IO.pipe
    wp.write_nonblock ("F" * i) while(select [], [wp], [], 0)
  rescue Errno::EAGAIN
    puts "#{i}: FAIL"
    passes = false
  ensure
    rp.close
    wp.close
  end
  puts "#{i}: PASS" if passes
end

我不能确定这是一个bug还是对规范的错误解释。你有什么想法吗?


1
我不知道这是否有帮助,但在Mac OS X 10.7.5上,我得到了以下输出(抱歉,它必须是一个注释,格式会很糟糕):1: byte count 15873, PASS 2: byte count 15874, PASS 3: byte count 15873, PASS 4: byte count 15876, PASS 5: byte count 15875, PASS 6: byte count 15876, PASS 7: byte count 15876, PASS 8: byte count 15880, PASS 9: byte count 15876, PASS 10: byte count 15880, PASS 11: byte count 15873, PASS 12: byte count 15876, PASS 13: byte count 15873, PASS 14: byte count 15876, PASS 15: byte count 15885, PASS . - Jonathan Leffler
1
POSIX规范中对于select()的说明是:“当使用一个没有设置O_NONBLOCK标志的输出函数调用不会阻塞时,描述符应被视为准备好写入,无论该函数是否成功传输数据”,但我不确定select()如何做出这个决定,因为它无法知道请求输出多少字节。在我看来,规范可能应该这样说:“...使用一个没有设置O_NONBLOCK标志的输出函数调用传输一个字节不会阻塞...”。 - Michael Burr
我看到10.8.2版本中出现了故障,所以我认为这是在Mountain Lion中引入的错误。ML还有一个“功能”,即它将扩展管道到64kB,这似乎是所有故障的共同点。 - Logan Bowers
在64位Linux上,我得到了相同类型的结果(但使用64k缓冲区)。@JonathanLeffler - alk
@MichaelBurr,或者规范可以说明当描述符具有足够的空间来满足描述符的原子性保证时,select()将描述符描述为就绪状态(例如,对于套接字,需要1字节的空间,但对于管道,则需要PIPE_BUF空间)。由于测试通过,Linux似乎采用了这种解释。 - Logan Bowers
如果您运行已编译的 <a href="http://www.chiark.greenend.org.uk/~peterb/linux/nonblock/">pipe-testnonblock.c</a>,输出是什么?请将 <code>signal.h</code> 添加到源文件的包含语句中,并使用以下命令进行编译:<code>gcc -Wall -Wextra -o pipe-testnonblock pipe-testnonblock.c</code>。 - user2008517
1个回答

3
你在这里使用了管道。 管道具有有趣的原子写入属性 --- 小于PIPE_BUF(此处为4096)字节的写入保证是原子的。 因此,即使可以向管道写入较小数量的字节,写入管道可能会因EAGAIN而失败。
我不确定你是否遇到了这种情况(我没有仔细查看),但是这种行为在man 7 pipe中有记录。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接