select
我们调用 select 告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等待多长时间。不局限于套接字,任何描述符都可以使用 select 来测试。select 与recv 和 send 直接操作文件描述符不同,它先对需要操作的文件描述符进行查询,查看是否目标文件描述符可以进行读、写或者错误操作,然后当文件描述符满足操作的条件的时候才进行真正的IO操作。
函数原型
select 函数原型如下:
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout);
// 成功返回就绪描述符的个数,超时返回0,失败返回-1
1.maxfdp1,指定待测试的描述符个数,它的值是待测试的最大描述符加1,描述符0,1,2,…,一直到 maxfdp1 - 1 均被测试。
头文件 <sys/select.h> 中定义的 FD_SETSIZE 常值时数据类型 fd_set 中的描述符总数,其值通常是1024
2.fd_set,一个表示描述符集的数据类型,具体的实现细节被隐藏在 fd_set 和以下四个宏中:
void FD_ZERO(fd_set* fdset); // clear all bits in fdset
void FD_SET(int fd, fd_set* fdset); // turn on the bit for fd in fdset
void FD_CLR(int fd, fd_set* fdset); // turn off the bit for fd in fdset
int FD_ISSET(int fd, fd_set* fdset); // is the bit for fd on in fdset?
描述符在描述符集中通常被称为位(bit),例如”打开读集合中表示监听描述符的位“。
readset、writeset 和 exceptset 分别是指向读、写、异常条件的描述符集的指针。
3.timeval,一个表示时间的结构,其结构如下:
struct timeval
{
long tv_sec; // seconds
long tv_usec; // microseconds
};
timeout,告知内核等待所指定描述符中的任何一个就绪可花多长时间。
将第三个参数设置成空指针表示 select 函数会一直等待下去,直到有一个描述符就绪。
函数作用
:select 函数修改由指针 readset、writeset 和 exceptset 所指向的描述符集,这三个参数都是值-结果参数。调用该函数时,指定所关心的描述符的值,该函数返回时,结果将指示哪些描述符已就绪。该函数返回后,可以通过 FD_ISSET 宏来测试 fd_set 数据类型中的描述符。描述符集内任何与未就绪描述符对应的位返回时均清成0。
描述符就绪条件
可读性和可写性对于普通文件描述符显而易见,但对于套接字描述符需要被讨论得更加明确。
当满足下列四个条件中的任何一个时,套接字可读:
1.该套接字接收缓冲区的数据字节数大于等于套接字接受缓冲区低水位标记的当前大小(其值默认为1)。
2.该连接的读半部关闭(接受了 FIN 的 TCP 连接)。对这样的套接字的读操作将不阻塞并返回0(也就是返回 EOF)。
3.该套接字是一个接听套接字且已完成的连接数不为0。对这样的套接字的 accept 通常不会阻塞。
4.其上有一个套接字错误待处理。
当满足下列四个条件中的任何一个时,套接字可写:
1.该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前值(其默认值为2048)。
2.该连接的写半部关闭。对这样的套接字的写操作将产生 SIGPIPE 信号。
3.使用非阻塞式 connect 的套接字已建立连接,或者 connect 已经以失败告终。
4.如果一个套接字存在带外数据或者仍处于带外标记,那么它有异常条件待处理。
当某个套接字上发生错误时,它将被 select 标记为即可读又可写。
select 使用例子
#include <sys/select.h>
#include <sys/time.h>
#include <sys/errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
using namespace std;
int main()
{
bool isIn = true;
char buf[1024];
int buflen = 0;
char* ptr;
fd_set fdset;
fd_set* pfdset = &fdset;
FD_ZERO(pfdset);
while(1)
{
// 由于每次select返回后会把未就绪的描述符位置为0,所以在循环开头要重新设置描述符位
if(isIn)
FD_SET(fileno(stdin), pfdset);
FD_SET(fileno(stdout), pfdset);
int maxfdp1 = max(fileno(stdin), fileno(stdout)) + 1;
if(select(maxfdp1, pfdset, pfdset, NULL, NULL) >= 0)
{
if(FD_ISSET(fileno(stdout), pfdset))
{
ptr = buf;
// 默认套接字发送缓冲区的低水位标记为2048,因此小于2048的字节在一次write调用中能全部写入缓冲区而不会在while循环中阻塞
while(buflen > 0)
{
int n;
if((n = write(fileno(stdout), ptr, buflen)) >= 0)
{
buflen -= n;
ptr += n;
}
else
exit(1);
}
if(!isIn)
break;
}
if(FD_ISSET(fileno(stdin), pfdset))
{
buflen = 0;
// 会读入换行符 \n
if((buflen = read(fileno(stdin), buf, 1024)) == 0)
{
isIn = false;
FD_CLR(fileno(stdin), pfdset);
}
else if(buflen < 0)
exit(1);
}
}
else
{
if(errno == EINTR)
continue;
else
exit(1);
}
}
exit(0);
}
pselect
#include <sys/select.h>
#include <signal.h>
#include <time.h>
int pselect(int maxfdp1, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timespec* timeout, const sigset_t* sigmask);
// 成功返回就绪描述符的数目,超时返回0,失败返回-1
pselect 相对于 select 有两个变化:
1.pselect 使用timespec结构,而不是用 timeval 结构。
struct timespec
{
time_t tv_sec; // seconds
long tv_nsec; // nanoseconds
};
2.pselect 增加了第六个参数:一个指向信号掩码的指针。
先看接下来两段代码:
// A1
if(intr_flag)
handle_intr();
// A2
if((nready = select(...)) < 0)
{
// A3
if(errno == EINTR)
{
if(intr_flag)
handle_intr();
}
...
}
sigset_t newmask, oldmask, zeromask;
sigemptyset(&zeromsak);
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
// B1
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
// B2
if(intr_flag)
handle_intr();
if((nready = pselect(..., &zeromask)) < 0)
{
// A3
if(errno == EINTR)
{
if(intr_flag)
handle_intr();
}
...
}
有一个前提和一个要求,前提是 SIGINT 的信号处理函数只会设置全局变量 intr_flag;要求是无论何时产生 SIGINT 信号,程序都必须执行 handle_intr 函数。
先看第一段代码,不管是在A1点还是A3点产生了SIGINT信号,程序都能够执行 handle_intr 函数。但是若在A2点产生了 SIGINT 信号,由于 select 是慢系统调用,程序可能一直不能执行 handle_intr 函数,或者是在 select 阻塞时产生了其他信号导致程序退出,并没有执行 handle_intr函数。而 pselect 能够避免这种情况的发生。
再看第二段代码,如果在B1点产生信号,信号掩码还没被更换,SIGINT 信号未被阻塞,所以 intr_flag 变量会被设置且程序能够执行 handle_intr。如果在B2点产生信号,由于信号掩码已经被更换,SIGINT 信号被阻塞,等到调用 pselect 时,它先以空集替代进程信号掩码,SIGINT 不被阻塞,信号处理函数完成后 pselect 被中断,并执行 handle_intr。如果在B3点产生 SIGINT 信号,由于在 pselect 阻塞时,进程掩码是空集,因此 SIGINT 信号不会被阻塞,pselect 会被中断并执行handle_intr。
pselect 返回时,进程的信号掩码会被重置为调用pselect之前的值。
poll
函数原型
#include <poll.h>
int poll(struct pollfd* fdarray, unsigned long nfds, int timeout);
// 成功返回就绪描述符数目,超时返回0,失败返回-1
1.第一个参数是指向一个结构数组第一个元素的指针,每个元素都是一个 pollfd 结构。pollfd 结构如下:
struct pollfd
{
int fd; // descriptor to check
short events; // events of interest on fd
short revents; // events that occurred on fd
};
要测试的条件由 events 成员指定,函数在相应的 revents 成员中返回该描述符的状态。这两个成员都由指定某个特定条件的一位或多位构成。
常值 | 作为 events 的输入 | 作为 revents 的结果 | 说明 |
---|---|---|---|
POLLIN | 1 | 1 | 普通或优先级带数据可读 |
POLLRDNORM | 1 | 1 | 普通数据可读 |
POLLRDBAND | 1 | 1 | 优先级带数据可读 |
POLLOUT | 1 | 1 | 普通数据可写 |
POLLWRNORM | 1 | 1 | 普通数据可写 |
POLLWRBAND | 1 | 1 | 优先级带数据可写 |
POLL ERR | 1 | 发生错误 | |
POLLHUP | 1 | 发生挂起 | |
POLLNVAL | 1 | 描述符不是一个打开的文件 |
2.第二个参数 nfds 指定结构数组中元素的个数。
3.第三个参数 timeout 指定 poll 函数返回前等待多长时间,单位为毫秒。INFTIM(通常为负数) 宏为永久等待,0为立即返回,正数等待指定数目的毫秒数。
引起 poll 返回特定的 revent 的条件
就 TCP 和 UDP 套接字而言,以下条件一起 poll 返回特定的 revent:
1.所有正规 TCP 数据 和 所有 UDP 数据都被认为是普通数据。
2.TCP 的带外数据被认为是优先级带数据。
3.当 TCP 连接的读半部关闭时,也被认为是普通数据,随后的读操作返回0。
4.TCP 连接存在错误即可认为是普通数据,也可认为是错误。无论那种情况,随后的读操作将返回-1,并把 errno 设置成合适的值。这可用于处理诸如接受到 RST 或发生超时条件。
5.在监听套接字上由新的连接可认为是普通数据,也可认为是优先级带数据。大多数视之为普通数据。
6.非阻塞式 connect 的完成被认识是使相应套接字可写。
poll 简单例子
#include <stdio.h>
#include <stdlib.h>
#include <poll.h>
#include <iostream>
using namespace std;
int main()
{
char buf[1024];
int buflen = 0;
struct pollfd fdarray;
fdarray.fd = fileno(stdin);
fdarray.events = POLLRDNORM | POLLRDBAND;
if(poll(&fdarray, 1, -1) >= 0)
{
if(fdarray.revents & POLLRDNORM)
{
cout << 1 << endl;
if((buflen = read(fdarray.fd, buf, 1024)) < 0)
{
exit(1);
}
char* ptr = buf;
// 同样由于套接字发送缓冲区低水位标记,一般在一次write调用中就完成输出
while(buflen)
{
int n;
if((n = write(fileno(stdout), ptr, buflen)) < 0)
{
exit(1);
}
ptr += n;
buflen -= n;
}
}
if(fdarray.revents & POLLRDBAND)
{
cout << 2 << endl;
if((buflen = read(fdarray.fd, buf, 1024)) < 0)
{
exit(1);
}
char* ptr = buf;
while(buflen)
{
int n;
if((n = write(fileno(stdout), ptr, buflen)) < 0)
{
exit(1);
}
ptr += n;
buflen -= n;
}
}
}
else
{
exit(1);
}
}