select()如何知道一个文件描述符变为“就绪”状态?

26

我不知道为什么很难找到这个,但我正在看一些Linux代码,其中我们使用select()等待文件描述符报告它已经准备就绪。从select的手册页中得到:

select() and pselect() allow a program to monitor multiple file descriptors,
waiting until one or more of the file descriptors become "ready" for some
class of I/O operation 

非常好...我在一些描述符上调用了select函数,设置了超时值并开始等待指示进行。文件描述符(或描述符的所有者)如何报告其“准备就绪”,以使select()语句返回?


1
http://beej.us/guide/bgnet/output/html/multipage/selectman.html - Nikolai Fetissov
1
@NikolaiNFetissov - 从你的链接中可以看到,select() 返回后,集合中的值会被改变以显示哪些套接字已准备好读取或写入,哪些套接字有异常。那么是什么导致了 select() 的返回告诉我们该套接字已准备好读取?这就是我不理解的地方。 - Mike
@NikolaiNFetissov - 你的意思是我打开一个fd并调用select,因为我想读取一些东西。在套接字的另一端,有人向该fd写入了数据,现在内核告诉select唤醒我,因为它已经“准备好”读取了? - Mike
3
是的,但select(2)(以及poll(2)epoll(7))的主要功能是I/O多路复用——您可以等待多个套接字并在事件发生时做出反应。 - Nikolai Fetissov
@NikolaiFetissov的链接到beej.us指南是404,截至2018年12月29日的新链接为:https://beej.us/guide/bgnet/html/multi/selectman.html - Noah Huppert
显示剩余2条评论
2个回答

32

它通过返回来报告它已准备就绪。

select等待通常在程序控制之外的事件。实质上,通过调用select,您的程序会说:“在...之前我没有什么要做,请暂停我的进程。”

您指定的条件是一组事件,其中任何一个都会唤醒您。

例如,如果您正在下载某些内容,则循环必须等待新数据到达,如果传输卡住则超时发生,或者用户中断,这正是select所做的。

当您有多个下载时,任何连接上到达的数据都会触发程序中的活动(您需要将数据写入磁盘),因此,您会将所有下载连接的列表给select以监视“read”的文件描述符列表。

当您同时将数据上传到某个地方时,您再次使用select查看连接是否当前接受数据。 如果对面正在拨号上网,则仅会缓慢确认数据,因此您的本地发送缓冲区始终是满的,任何尝试写入更多数据的尝试都将阻塞直到有可用的缓冲区空间为止,或者失败。 通过将我们要发送的文件描述符传递给select作为“write”描述符,我们将在发送缓冲区有可用的空间时立即收到通知。

总体思路是使您的程序成为事件驱动程序,即它从公共消息循环中对外部事件做出反应,而不是执行顺序操作。 您告诉内核“这是我要做某事的事件集”,内核会给您一组已发生的事件。 两个事件同时发生是相当常见的;例如,在数据包中包含了TCP确认,这可以使同一fd既可读(数据可用)又可写(确认的数据已从发送缓冲区中删除),因此您应准备在再次调用select之前处理所有事件。

其中一个精妙之处在于,select基本上给出了承诺,即一次readwrite的调用不会阻塞,但没有任何保证该调用本身。 例如,如果一个字节的缓冲区空间可用,则可以尝试写入10个字节,内核将返回并说:“我已经写入了1个字节”,因此您应准备处理这种情况。 典型的方法是拥有一个“要写入到此fd的数据”缓冲区,只要它不为空,就将fd添加到写入集中,并通过尝试写入当前在缓冲区中的所有数据来处理“可写”事件。 如果之后缓冲区为空,则好,如果不是,则再次等待“可写”。

“异常”集很少使用-它用于具有带外数据的协议,在该协议中,数据传输可能会被阻塞,而其他数据需要通过。 如果您的程序当前无法从“可读”文件描述符接受数据(例如,正在下载并且磁盘已满),则不要将描述符包括在“可读”集合中,因为您无法处理该事件并且如果再次调用select,它将立即返回。如果接收方在“exception”集合中包含fd,并且发送方要求其IP堆栈发送带有“紧急”数据的数据

#include <sys/types.h>
#include <sys/select.h>

#include <unistd.h>

#include <stdbool.h>

static inline int max(int lhs, int rhs) {
    if(lhs > rhs)
        return lhs;
    else
        return rhs;
}

void copy(int from, int to) {
    char buffer[10];
    int readp = 0;
    int writep = 0;
    bool eof = false;
    for(;;) {
        fd_set readfds, writefds;
        FD_ZERO(&readfds);
        FD_ZERO(&writefds);

        int ravail, wavail;
        if(readp < writep) {
            ravail = writep - readp - 1;
            wavail = sizeof buffer - writep;
        }
        else {
            ravail = sizeof buffer - readp;
            wavail = readp - writep;
        }

        if(!eof && ravail)
            FD_SET(from, &readfds);
        if(wavail)
            FD_SET(to, &writefds);
        else if(eof)
            break;
        int rc = select(max(from,to)+1, &readfds, &writefds, NULL, NULL);
        if(rc == -1)
            break;
        if(FD_ISSET(from, &readfds))
        {
            ssize_t nread = read(from, &buffer[readp], ravail);
            if(nread < 1)
                eof = true;
            readp = readp + nread;
        }
        if(FD_ISSET(to, &writefds))
        {
            ssize_t nwritten = write(to, &buffer[writep], wavail);
            if(nwritten < 1)
                break;
            writep = writep + nwritten;
        }
        if(readp == sizeof buffer && writep != 0)
            readp = 0;
        if(writep == sizeof buffer)
            writep = 0;
    }
}

我们尝试读取缓冲区是否有可用空间,且读取端没有到达文件结尾或出现错误;我们也尝试在缓冲区有数据时进行写入操作。如果已经到达文件结尾且缓冲区为空,则表示操作完成。

这段代码的效率显然不够高(它只是示例代码),但您应该能够看到,在读取和写入时内核允许执行不完全满足请求的情况。此时我们仅需回去并说“准备好后再通知我”,我们永远不会在未询问是否会阻塞的情况下进行读写操作。


你不应该假设读取(或写入)不会阻塞,因为在 select() 返回和 read() 或 write() 发出之间可能会发生某些事情。例如,其他人可能会读取数据/填充管道。它更像是一种操作有可能不阻塞的信号。此外,如果 fd 上存在错误条件,select() 将唤醒,因为这会导致立即返回带有错误条件的 read()/write()。 - rici
1
如果只有我访问这个文件描述符,那么至少对于套接字和管道来说,我有这个保证。如果有其他人访问我的文件描述符,我将会得到奇怪的交错数据。 - Simon Richter
是的,如果您在一个进程中打开了fd,并且它是未命名的管道或套接字,则可以。但如果它是命名管道,则不行。(从技术上讲,如果您能够以某种方式知道您是读取/写入命名管道的唯一进程,那么这是正确的,但是您如何知道呢?)我提出这个问题只是因为它是一个经典问题,也就是“为什么我的服务器会在随机时间间隔内冻结?” - rici
1
是的,为了增加安全性,可以将fd设置为非阻塞模式,并将产生的“EWOULDBLOCK”视为错误。 - Simon Richter
如果某一刻我们没有任何需要写入的数据,而“writefds”是一个空集合怎么办?我见过一些规范说明将空集合传递给“select()”是不正确的,因此你应该检查是否已实际添加了任何fd到“writefds”,如果没有,则应将NULL传递给“select()”,而不是“writefds”。 - JustAMartin
@JustAMartin,这不应该有任何影响。对于WinSock,我们需要检查任何集合中至少有一个fd被设置,否则它将返回错误--但这可以通过检查maxfd来完成。 - Simon Richter

8

同一手册页中的说明:

退出时,这些集合会被原地修改以指示哪些文件描述符实际上已更改状态。

因此,在使用传递给select的集合确定哪些FD已准备就绪时,请使用FD_ISSET()


2
我可能没理解到点上,我在问“是什么导致select()返回文件描述符是‘ready'”,而我听到的是,“当它们准备好了,select就会返回”。那么何为“准备就绪”的套接字的定义呢? - Mike
2
可以被读取、写入或发生其他异常情况,具体取决于它所在的集合。"在readfds中列出的将被监视以查看是否有字符可供读取(更准确地说,查看是否不会阻塞读取;特别地,文件描述符在文件结束时也准备好了),在writefds中列出的将被监视以查看是否不会阻塞写入,在exceptfds中列出的将被监视以查看是否有异常情况。" - Ignacio Vazquez-Abrams
这正是我在寻找的。 - Jacob Garby

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