【问题标题】:how is select() alerted to an fd becoming "ready"?select() 如何提醒 fd “准备好”?
【发布时间】:2012-09-19 11:10:50
【问题描述】:

我不知道为什么我很难找到它,但我正在查看一些 linux 代码,其中我们使用 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() 语句返回?

【问题讨论】:

  • @NikolaiNFetissov - 来自您的链接,After select() returns, the values in the sets will be changed to show which are ready for reading or writing, and which have exceptions. 那么是什么导致select() 返回告诉我们套接字已准备好读取?这就是我不明白的地方
  • 当内核网络堆栈检测到任何套接字描述符上存在未决事件时,您的进程将从等待中唤醒,select 返回。 FD 集是输入输出参数——你告诉内核你感兴趣的是什么,它告诉你发生了什么。
  • @NikolaiNFetissov - 所以你说我打开一个 fd 并打电话给select 因为我想读点东西。在套接字的另一端,有人写入了该 fd,现在内核告诉 select 唤醒我,因为它“准备好”读取?
  • 是的,但是select(2)(和poll(2),或epoll(7))的主要功能是I/O解复用 - 你可以等待多个套接字并在事件发生时做出反应。

标签: c linux sockets messaging file-descriptor


【解决方案1】:

它报告它已经准备好通过返回。

select 等待通常不在您的程序控制范围内的事件。本质上,通过调用select,您的程序会说“我无事可做,直到...,请暂停我的进程”。

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

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

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

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

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

其中一个更好的点是select 基本上向您保证,readwrite 的一次调用不会阻塞,而不对调用本身做任何保证。例如,如果有 1 个字节的缓冲区空间可用,您可以尝试写入 10 个字节,内核会返回并说“我已写入 1 个字节”,因此您也应该准备好处理这种情况。一个典型的做法是有一个缓冲区“要写入这个fd的数据”,只要它不为空,就将fd加入到write set中,通过尝试将所有的写入来处理“writeable”事件当前在缓冲区中的数据。如果之后缓冲区为空,则可以,如果没有,请再次等待“可写”。

“异常”集很少使用——它用于具有带外数据的协议,其中数据传输可能会阻塞,而其他数据需要通过。如果您的程序当前无法接受来自“可读”文件描述符的数据(例如,您正在下载,并且磁盘已满),您不希望将描述符包含在“可读”集中,因为您无法处理该事件如果再次调用,select 将立即返回。如果接收方将 fd 包含在“异常”集中,并且发送方要求其 IP 堆栈发送带有“紧急”数据的数据包,则接收方将被唤醒,并可以决定丢弃未处理的数据并与发送方重新同步. telnet 协议使用它,例如,用于 Ctrl-C 处理。除非您正在设计需要此类功能的协议,否则您可以轻松地忽略它而不会造成任何伤害。

强制代码示例:

#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() 立即返回错误条件。
  • 如果我是唯一一个访问这个文件描述符的人,那么我有这个保证,至少对于套接字和管道来说。如果其他人访问我的 fds,无论如何我都会得到奇怪的交错数据。
  • 是的,如果您只在一个进程中打开了 fd,并且它是一个未命名的管道或套接字。但如果它是命名管道,则不是。 (从技术上讲,如果你能以某种方式知道你是读取/写入命名管道的唯一进程,那是真的,但你怎么知道?)我提出这一点只是因为这是一个经典问题,即“为什么我的服务器冻结在随机间隔?”
  • 是的,为了更加安全,可以将 fd 设置为非阻塞模式,并将生成的 EWOULDBLOCK 视为错误。
  • 如果在某一时刻我们没有什么可写的并且writefds 是空集怎么办?我看到一些规范说将空集传递给select() 是不正确的,所以你应该检查是否有任何fd 实际添加到writefds,如果没有,则将NULL 传递给select() 而不是@ 987654339@.
【解决方案2】:

来自同一个手册页:

在退出时,集合被修改以指示哪些文件描述符实际更改了状态。

因此,在传递给 select 的集合上使用 FD_ISSET() 以确定哪些 FD 已准备好。

【讨论】:

  • 我一定错过了这里的重点,我在问“导致 select() 返回文件描述符的原因是 ready”,我听到的是,“当它们是 @ 时选择返回987654323@,因为他们准备好了”。 “就绪”套接字的定义是什么?
  • 一个可以读取、写入或发生其他异常事件的字符,具体取决于它所在的集合。“将监视 readfds 中列出的字符以查看字符是否可用于读取(更准确地说,查看读取是否不会阻塞;特别是文件描述符在文件结束时也已准备好),writefds 中的那些将被监视以查看写入是否不会阻塞,而 exceptfds 中的那些将被监视异常。”
  • 正是我要找的。​​span>
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-08-25
  • 1970-01-01
  • 2013-03-11
  • 2015-10-29
  • 1970-01-01
相关资源
最近更新 更多