与阻塞 select() 一起使用时,非阻塞套接字真的是非阻塞的吗?

Is non-blocking socket really non-blocking when used with blocking select()?

这是一个比较理论化的问题。如果套接字 I/O(readwrite)设置为 O_NONBLOCK,但此套接字在 fd_set 中设置为 select(),其中 blocks(等待文件描述符变为可读或可写的事件),然后该套接字无论如何都是 blocking(由于 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);
                    }
                }
            }
        }
    }
}

This is rather theoretical question. If sockets I/O (either read or write) is set to O_NONBLOCK, but then this socket is set in fd_set to select() which blocks (waiting for an event the file descriptor become either readable or writable), then that socket is blocking anyway (due to the select())?

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

Why would I set the socket to be non-blocking, when even the blocking (default) version once become readable (or writable) (thanks to select()), won't block, because the select() said it has data to read (or write) and thus the socket is able to perform its operation with that data without blocking.

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

So why to bother setting socket non-blocking when select() blocks anyway?

因为您不希望套接字上的操作被阻塞。 select 函数不能保证未来的操作不会阻塞,过去人们已经因为做出这种假设而被烧毁。

例如,您在 UDP 套接字上执行 select,它表示接收不会阻塞。但在您调用 recv 之前,管理员启用了之前禁用的 UDP 校验和。你猜怎么着,现在如果唯一接收到的数据报的校验和不正确,你的 recv 将被阻止。

除非您认为您可以预见到这种事情可能发生的所有方式,而且您绝对不能,否则如果您不希望套接字阻塞,则必须将其设置为非阻塞。

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

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

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