我不知道为什么我很难找到它,但我正在查看一些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()
语句返回?
它通过返回来报告它已经准备好了。
select
等待通常超出程序控制范围的事件。从本质上讲,通过调用select
,您的程序会说"在…之前我没有什么可做的,请暂停我的进程"。
您指定的条件是一组事件,其中任何一个都会唤醒您。
例如,如果你正在下载一些东西,你的循环将不得不等待新数据到达,如果传输被卡住,则会发生超时,或者用户会中断,这正是select
所做的。
当你有多次下载时,任何连接上的数据都会触发程序中的活动(你需要将数据写入磁盘),所以你应该在文件描述符列表中列出所有到select
的下载连接,以查看"读取"。
当您同时将数据上传到某个位置时,您将再次使用select
来查看连接当前是否接受数据。如果另一端正在拨号,它只会缓慢地确认数据,因此本地发送缓冲区总是满的,任何写入更多数据的尝试都会被阻止,直到缓冲区空间可用,或者失败。通过将我们要发送的文件描述符作为"写入"描述符传递给select
,我们将在缓冲区空间可用于发送时立即收到通知。
一般的想法是,您的程序变成事件驱动,即它对来自公共消息循环的外部事件做出反应,而不是执行顺序操作。你告诉内核"这是我想做的一组事件",内核会给你一组已经发生的事件。两个事件同时发生是很常见的;例如,TCP确认包含在数据包中,这可以使同一fd可读(数据可用)和可写(已确认的数据已从发送缓冲区中删除),因此在再次调用select
之前,您应该准备好处理所有事件。
更精细的一点是,select
基本上为您提供了一个承诺,即read
或write
的一次调用不会阻塞,而不会对调用本身做出任何保证。例如,如果有一个字节的缓冲区空间可用,您可以尝试写入10个字节,内核会回来说"我已经写入了1个字节",所以您也应该准备好处理这种情况。一种典型的方法是让一个缓冲区"data to be write to this fd",只要它不是空的,fd就会被添加到写入集,并且通过尝试写入缓冲区中当前的所有数据来处理"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;
}
}
如果我们有可用的缓冲区空间,并且读取端没有文件结尾或错误,我们会尝试读取,如果缓冲区中有数据,我们会试图写入;如果到达文件末尾并且缓冲区为空,那么我们就完成了。
这段代码的表现显然是次优的(这是示例代码),但你应该能够看到,内核在读写方面做得比我们要求的少是可以接受的,在这种情况下,我们只会回过头来说"只要你准备好了",而且我们在读写时永远不会问它是否会阻塞。
来自同一手册页:
在退出时,会在适当的位置修改集合,以指示哪些文件描述符实际更改了状态。
因此,对传递的集合使用FD_ISSET()
进行选择,以确定哪些FD已准备就绪。