当读缓冲区大小为2048时,它可以工作,但是缓冲区大小为48或其他数字时,写调用将被阻塞,为什么?

当读缓冲区大小为2048时,它可以工作,但是缓冲区大小为48或其他数字时,写调用将被阻塞,为什么?
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char* argv[]){
    if(argc < 2){ 
        puts("err argv");
        return -1; 
    }   

    int r_fd = open(argv[1], O_RDWR);
    int w_fd = open(argv[1], O_RDWR);

    fd_set r_set;
    fd_set w_set;
    char w_buf[4096];
    char r_buf[2048];
    int read_count = 0,write_count = 0;  
    while(1){
        FD_ZERO(&r_set);
        FD_ZERO(&w_set);
        FD_SET(r_fd, &r_set);
        FD_SET(w_fd, &w_set);
        int ret = select(w_fd + 1, &r_set, &w_set, NULL, NULL);
        if(FD_ISSET(r_fd, &r_set)){
            read(r_fd, r_buf, sizeof(r_buf));    
            printf("read count:%d\n", read_count++);    
        }   
        if(FD_ISSET(w_fd, &w_set)){
            write(w_fd, w_buf, sizeof(w_buf));
            printf("write count:%d\n", write_count++);  
        }   
        //sleep(1);
    }   

    return 0;
}

执行代码:mkfifo 1.fifo && gcc main.c -o main && ./main 1.fifo

答案1

        if(FD_ISSET(w_fd, &w_set)){
            write(w_fd, w_buf, sizeof(w_buf));
            printf("write count:%d\n", write_count++);  
        }   

如果select告诉你一个 fd 是可写的,那就意味着你可以写至少一个字节。该阈值可能高于 1,具体取决于 FIFO 缓冲区的管理方式,但这当然并不意味着您可以无阻塞地写入 4096 字节。

该代码在静态上是错误的,必须将两个 fd 设置为非阻塞模式(并准备好适当地处理EAGAIN/EWOULDBLOCK返回代码)才能相当健壮。

原因w_fd某些读取缓冲区大小暴露的问题与您是否可以达到 FIFO 足够空(显示为可写)但又太满而无法完成写入(因此会阻塞)的情况有关。

您可以通过将调试器附加到被阻止的进程或在 strace 下运行它来简单地确认或伪造这一点。这些都是您应该学习使用的完全正常的工具。

答案2

问题似乎出在单次选择后检查 r_set 和 w_set 时——可能是读取或写入使结果无效的竞争条件。

这是一个强大的版本,带有额外的调试和一些结果。

在写入之前进行任何可用的读取非常重要。如果您给予写入优先权,它将在读取之前填充 fifo(可能最多 64KB)。这可能是原始代码中竞争条件的一部分。

//  xSelect.c

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

int main(int argc, char* argv[]){
    if(argc < 2){
        puts("err argv");
        return -1;
    }
    ssize_t r_res, w_res;
    int r_fd, w_fd;
    r_fd = open(argv[1], O_RDWR);
    w_fd = open(argv[1], O_RDWR); 
    fprintf (stderr, "r_fd %d, w_fd %d\n", r_fd, w_fd);

    fd_set r_set;
    fd_set w_set;
    char w_buf[4096];
    char r_buf[1235];

    for (int j = 0, w = 0; j < 20; j++) {
        FD_ZERO(&r_set);
        FD_ZERO(&w_set);
        FD_SET(r_fd, &r_set);
        FD_SET(w_fd, &w_set);
        int ret = select(w_fd + 1, &r_set, &w_set, NULL, NULL);
        if(FD_ISSET(r_fd, &r_set)){
            r_res = read(r_fd, r_buf, sizeof(r_buf));
            fprintf (stderr, "Read %zd\n", r_res);
        } else if(w++ < 3 && FD_ISSET(w_fd, &w_set)){
            w_res = write(w_fd, w_buf, sizeof(w_buf));
            fprintf (stderr, "Write %zd\n", w_res);
        } else {
            fprintf (stderr, "Tick\n");
        }
    }
    return 0;
}
$ make xSelect && echo Run && ./xSelect xSelect.fifo
cc xSelect.c -o xSelect
Run
r_fd 3, w_fd 4
Write 4096
Read 1235
Read 1235
Read 1235
Read 391
Write 4096
Read 1235
Read 1235
Read 1235
Read 391
Write 4096
Read 1235
Read 1235
Read 1235
Read 391
Tick
Tick
Tick
Tick
Tick
$ 

编辑:我可能想得太多了。我不认为 r_set 和 w_set 直接受到单个读取和/或写入的影响:但是它们已被这些操作所淘汰。

在您的原始代码中,w_fd始终可写,直到 FIFO 完全满为止。因此,如果您执行相同数量的长写入和短读取,写入器最终将挂起。如果您的短读取是 2048 字节,那么您将需要大约 32 个周期来填充 fifo:如果短读取是 48,您将用 17 个周期填充 fifo。

显然,从长远来看,平均写作率和阅读率必须相等。通常,内核通过不调度写入器(位于不同的进程中)来实现这一点,但您的代码强制执行一次读取,一次写入。所以w_set.w_fd最终会变得不稳定。

一个问题是 select() 不知道您打算写入多少数据。如果 fifo 有一个字节的空间,则 w_fd 是可写的。我不知道写入是否被截断、阻塞或失败,也不知道两个 fd 使用相同的 fifo 是否有任何区别。它甚至不值得测试,因为答案可能取决于操作系统,以及我们使用的是 fifo 还是套接字,或者其他一些因素。您的代码应该编写为处理部分写入,返回 EAGAIN 的错误,阻塞。

您的代码会因完全填满 fifo 而受到影响,而我的代码则会因每次写入后不必要地完全清空 fifo 而受到影响。适当的演示应该调整(或随机化)其行为以探索可能性。

相关内容