【问题标题】:Is non-blocking socket really non-blocking when used with blocking select()?与阻塞 select() 一起使用时,非阻塞套接字真的是非阻塞的吗?
【发布时间】:2023-03-19 13:03:02
【问题描述】:

这是一个相当理论的问题。如果套接字 I/O(readwrite)设置为 O_NONBLOCK,但此套接字在 fd_set 中设置为 select()阻塞(等待事件文件描述符变为可读或可写),那么该套接字无论如何阻塞(由于select())?

为什么我要将套接字设置为非阻塞,即使阻塞(默认)版本一旦变为可读(或可写)(感谢select()),也不会阻塞,因为select() 说过具有要读取(或写入)的数据,因此套接字能够使用该数据执行其操作而不会阻塞。那么,当 select() 阻塞时,为什么还要设置套接字非阻塞呢?

具有non-block 套接字的示例,阻塞 select():

#include <unp.h>

void str_cli(FILE *fp, int sockfd)
{
    int flags, maxfd, stdineof;

    fd_set rset, wset;

    ssize_t nbytes, nactual;
    //struct bufpos { char *read_ptr, *write_ptr; };
    struct bufpos ipos, opos;
    char inbuf[BSIZE], outbuf[BSIZE];

    //--//

    //set nonblocking flag for these fds:
    int nblkFds[3] = {sockfd, STDIN_FILENO, STDOUT_FILENO};
    for (int i = 0; i < 3; i++)
    {
        flags = Fcntl(nblkFds[i], F_GETFL, 0);
        Fcntl(nblkFds[i], F_SETFL, flags | O_NONBLOCK);
    }

    //initialize buffer positions
    ipos.write_ptr = ipos.read_ptr = inbuf;
    opos.write_ptr = opos.read_ptr = outbuf;

    stdineof = 0; //stdin
    maxfd = max(STDOUT_FILENO, sockfd) + 1;

    while (1)
    {
        FD_ZERO(&rset);
        FD_ZERO(&wset);
        //can read from stdin and readptr is not at the end of buffer
        if (stdineof == 0 && opos.read_ptr < &outbuf[BSIZE])
        {
            FD_SET(STDIN_FILENO, &rset);
        }
        //can read from socket and the readptr is not at then end of buffer
        if (ipos.read_ptr < &inbuf[BSIZE])
        {
            FD_SET(sockfd, &rset);
        }
        //difference in outbuf == data to write to socket
        if (opos.read_ptr != opos.write_ptr)
        {
            FD_SET(sockfd, &wset);
        }
        //difference in inbuf == data to write to file
        if (ipos.read_ptr != ipos.write_ptr)
        {
            FD_SET(STDOUT_FILENO, &wset);
        }

        Select(maxfd, &rset, &wset, NULL, NULL);

        if (FD_ISSET(STDIN_FILENO, &rset))
        {
            switch ((nbytes = read(STDIN_FILENO, opos.read_ptr, &outbuf[BSIZE] - opos.read_ptr)))
            {
            case -1:
                perror("read");
                if (errno != EWOULDBLOCK)
                {
                    die("read");
                }

            case 0:
                fprintf(stderr, "%s: EOF on stdin\n", nowtime());
                stdineof = 1;
                if (opos.write_ptr == opos.read_ptr)
                {
                    //everything was written to socket -> we won't be writing enything else -> close the connection by sending FIN
                    Shutdown(sockfd, SHUT_WR);
                }
                break;

            default:
                fprintf(stderr, "%s: read %ld bytes from stdin\n", nowtime(), nbytes);
                //move the read pointer with bytes writen
                opos.read_ptr += nbytes;
                //now those bytes could be writen to socket
                FD_SET(sockfd, &wset);
            }
        }

        if (FD_ISSET(sockfd, &rset))
        {
            switch ((nbytes = read(sockfd, ipos.read_ptr, &inbuf[BSIZE] - ipos.read_ptr)))
            {
            case -1:
                perror("read");
                if (errno != EWOULDBLOCK)
                {
                    die("read");
                }

            case 0:
                fprintf(stderr, "%s: EOF on socket\n", nowtime());
                if (stdineof)
                {
                    //normal termination (client EOF)
                    return;
                }
                else
                {
                    //RST from peer
                    die("str_cli: server terminated prematurely");
                }
                break;

            default:
                fprintf(stderr, "%s: read %ld bytes from socket\n", nowtime(), nbytes);
                //move the read pointer with bytes read
                ipos.read_ptr += nbytes;
                //those bytes could be writen to file
                FD_SET(STDOUT_FILENO, &wset);
            }
        }

        if (FD_ISSET(STDOUT_FILENO, &wset) && (nbytes = ipos.read_ptr - ipos.write_ptr) > 0)
        {
            //the stdout is writeable and there are some bytes to write
            switch ((nactual = write(STDOUT_FILENO, ipos.write_ptr, nbytes)))
            {
            case -1:
                perror("write");
                if (errno != EWOULDBLOCK)
                {
                    die("write");
                }
            default:
                fprintf(stderr, "%s: wrote %ld bytes to stdout\n", nowtime(), nactual);
                ipos.write_ptr += nactual;
                if (ipos.write_ptr == ipos.read_ptr)
                {
                    //back to beginning buffer if all was writen to stdout
                    ipos.write_ptr = ipos.read_ptr = inbuf;
                }
            }
        }

        if (FD_ISSET(sockfd, &wset) && ((nbytes = opos.read_ptr - opos.write_ptr) > 0))
        {
            //the socket is writeable and there are some bytes to write
            switch ((nactual = write(sockfd, opos.write_ptr, nbytes)))
            {
            case -1:
                perror("write");
                if (errno != EWOULDBLOCK)
                {
                    die("write");
                }

            default:
                fprintf(stderr, "%s wrote %ld bytes to socket\n", nowtime(), nactual);
                opos.write_ptr += nactual;
                if (opos.write_ptr == opos.read_ptr)
                {
                    //back to beginning buffer if all was send/writen to socket
                    opos.read_ptr = opos.write_ptr = outbuf;
                    if (stdineof)
                    {
                        //EOF, could send its FIN
                        Shutdown(sockfd, SHUT_WR);
                    }
                }
            }
        }
    }
}

【问题讨论】:

  • select 忽略 O_NONBLOCK 但它并没有改变套接字实际上仍然是非阻塞的事实。 当 select() 阻塞时为什么要设置套接字非阻塞。为什么确实如此。你见过这样做的代码吗?可能有这样的用例,但也可能不常见。
  • @kaylum 好吧,我可以给你一个来自 Addison Wesley 的例子:UNIXNetwork ProgrammingVolume 1。见编辑。我认为它的例子相当复杂,但这个例子是我问的原因
  • @kaylum 所有使用非阻塞套接字的代码都必须这样做,否则套接字会阻塞。看我的回答。

标签: c sockets select nonblocking


【解决方案1】:

这是一个相当理论的问题。如果套接字 I/O(读或写)设置为 O_NONBLOCK,但随后此套接字在 fd_set 中设置为 select() 阻塞(等待文件描述符变为可读或可写的事件),则该套接字阻塞无论如何(由于 select())?

select 正在阻塞。套接字仍然是非阻塞的。

为什么我要将套接字设置为非阻塞,即使阻塞(默认)版本一旦变得可读(或可写)(感谢 select()),也不会阻塞,因为 select() 说过有数据要读取(或写入),因此套接字能够使用该数据执行其操作而不会阻塞。

不,不,不!这不是一个安全的假设。无法保证后续的readwrite 不会阻塞。如果您需要将来保证以后的操作不会阻塞,则必须将套接字设置为非阻塞。

那么,当 select() 阻塞时,为什么还要设置套接字非阻塞呢?

因为您不希望套接字上的操作被阻塞。 select 函数并不能保证未来的操作不会阻塞,并且人们在过去的假设中已经被烧毁了。

例如,您在 UDP 套接字上执行 select 并表示接收不会阻塞。但在您致电recv 之前,管理员会启用之前禁用的UDP 校验和。猜猜看,现在你的recv 将在唯一收到的数据报上校验和不正确时阻塞。

除非你认为你可以预见到这样的事情可能发生的每一种方式,而你绝对不能,如果你不希望它阻塞,你必须将套接字设置为非阻塞。

【讨论】:

  • read can 阻止是什么意思,即使在select() 说它有一些数据要读取之后?那么select() 是为了什么,当它给出这些错误的假设时呢?我认为select() returns 当且仅当它得到一些内核管理的 I/O event 时。所以如果一个文件描述符得到了一个event,那么就没有理由block。状态机在 select() 中很简单。有一些数据(通过一些 io 事件)? -> 返回。不? -> 阻止。为什么后续调用会阻塞(根据您的说法)?当这是 select() 的工作?
  • @milanHrabos 这就像在写入之前检查磁盘空间一样。如果磁盘已满,您可能需要等到有可用磁盘空间。但是磁盘空间的存在当您检查时并不能保证后续写入将有足够的磁盘空间。 select 函数只是一个状态报告函数,可以告诉您过去的情况。它不对未来的运营提供任何保证。我解释了为什么后续调用可能会阻塞——你没有想到的事情可能会在这两者之间发生。
  • @milanHrabos 如果您有信心可以想到所有系统(过去、现在和未来)上可能发生的每一种奇怪的情况,然后继续掷骰子。但是人们已经被这种假设严重烧伤了。一个非常严重的 Linux inetd DoS 攻击就是这个错误的一个例子。
  • 好的,你明白了。但我们谈论的是一小段时间,在select() 返回和read 开始执行之间。通常那些是后续调用,一个接一个,所以时间跨度应该非常小。但你是对的,它存在
  • @milanHrabos 该过程可以在选择和读取之间被抢占,并且可以根据需要将“少量时间”设置得尽可能大。但是,即使您设置了非阻塞标志,通过 another 文件描述符共享同一个打开套接字的其他进程也可以关闭非阻塞标志,并且您的读取仍然会阻塞。道德:尽可能使用recv(MSG_DONTWAIT)(非标准,但在大多数 Unix 平台上都支持)。
【解决方案2】:

select 忽略文件描述符上的非阻塞标志,因为关注它没有任何意义。

  • select 正在(可能)同时检查多个文件描述符,这些文件描述符可能具有不同的非阻塞标志。应该注意哪些?
  • select 有自己的显式超时,用于确定调用是阻塞还是非阻塞,还是在有限时间内阻塞。

可以说,可以在文件描述符上设置非阻塞“状态”标志是一个糟糕的设计——最好在每次调用时指定是阻塞还是非阻塞。事实上,如果您使用recvsend 而不是readwrite,您可以做到这一点。所有在文件描述符上设置“非阻塞”的真正作用是它发出的调用没有以其他方式指定阻塞或非阻塞、非阻塞。

【讨论】:

  • 我知道非阻塞套接字和阻塞select()操作之间没有关系。 (该操作对其fds 一无所知)。但是由于select()overall 程序显示为阻塞。那么,为什么要将fds 设置为非阻塞并将它们与select() 结合使用呢?
  • 如果你只是想要一个阻塞或非阻塞读取,你为什么要使用 select?
  • “非阻塞状态标志 [...] 是一个糟糕的设计”,尤其是因为它是用于读写的单一标志——在终端中运行 foo | bar | baz 时,baz在不破坏foo 的标准输入的情况下,无法在其标准输出上执行非阻塞 i/o。套接字有MSG_DONTWAIT,但管道和ttys没有这样的东西。
  • 并且该标志并未真正设置在文件描述上,而是设置在文件描述ion上——所有fds都指向同一个打开套接字或文件共享相同的O_NONBLOCK 标志。
  • @milanHrabos 程序阻塞,但它不会阻塞套接字在套接字上的操作是非阻塞的(我们希望,假设它被编码)。
猜你喜欢
  • 1970-01-01
  • 2010-10-31
  • 2013-10-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-12
  • 1970-01-01
相关资源
最近更新 更多