它通过返回来报告它已准备就绪。
select
等待通常在程序控制之外的事件。实质上,通过调用select
,您的程序会说:“在...之前我没有什么要做,请暂停我的进程。”
您指定的条件是一组事件,其中任何一个都会唤醒您。
例如,如果您正在下载某些内容,则循环必须等待新数据到达,如果传输卡住则超时发生,或者用户中断,这正是select
所做的。
当您有多个下载时,任何连接上到达的数据都会触发程序中的活动(您需要将数据写入磁盘),因此,您会将所有下载连接的列表给select
以监视“read”的文件描述符列表。
当您同时将数据上传到某个地方时,您再次使用select
查看连接是否当前接受数据。 如果对面正在拨号上网,则仅会缓慢确认数据,因此您的本地发送缓冲区始终是满的,任何尝试写入更多数据的尝试都将阻塞直到有可用的缓冲区空间为止,或者失败。 通过将我们要发送的文件描述符传递给select
作为“write”描述符,我们将在发送缓冲区有可用的空间时立即收到通知。
总体思路是使您的程序成为事件驱动程序,即它从公共消息循环中对外部事件做出反应,而不是执行顺序操作。 您告诉内核“这是我要做某事的事件集”,内核会给您一组已发生的事件。 两个事件同时发生是相当常见的;例如,在数据包中包含了TCP确认,这可以使同一fd既可读(数据可用)又可写(确认的数据已从发送缓冲区中删除),因此您应准备在再次调用select
之前处理所有事件。
其中一个精妙之处在于,select
基本上给出了承诺,即一次read
或write
的调用不会阻塞,但没有任何保证该调用本身。 例如,如果一个字节的缓冲区空间可用,则可以尝试写入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()
返回后,集合中的值会被改变以显示哪些套接字已准备好读取或写入,哪些套接字有异常。那么是什么导致了select()
的返回告诉我们该套接字已准备好读取?这就是我不理解的地方。 - Mikeselect
,因为我想读取一些东西。在套接字的另一端,有人向该fd写入了数据,现在内核告诉select
唤醒我,因为它已经“准备好”读取了? - Mikeselect(2)
(以及poll(2)
或epoll(7)
)的主要功能是I/O多路复用——您可以等待多个套接字并在事件发生时做出反应。 - Nikolai Fetissov