I/O复用:select和poll函数
1. 概述
进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪,它就通知进程。这个能力称为 I/O 复用。
2. I/O模型
- 阻塞式I/O;
- 非阻塞式I/O;
- I/O复用(select和poll);
- 信号驱动式I/O(SIGIO);
- 异步I/O 。
2.1 阻塞式I/O模型
默认情况下,所有套接字都是阻塞的。
进程调用 recvfrom ,其系统调用直到数据报到达且被复制到应用进程的缓冲区中发生错误才返回。
2.2 非阻塞式I/O模型
进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。
当一个应用进程像这样循环调用 recvfrom 时,我们称之为轮询(polling)。
2.3 I/O 复用模型
有了I/O复用,就可以调用select和poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的I/O系统调用上。
使用 select 的优势在于我们可以等待多个描述符就绪。
2.4 信号驱动式I/O模型
用信号,让内核在描述符就绪时发送 SIGIO 信号通知我们。称这种模型为信号驱动式I/O 。
2.5 异步 I/O 模型
信号驱动式I/O是由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。
该信号直到数据已复制到应用进程缓冲区才产生,这一点不同于信号驱动式I/O模型。
2.6 各种I/O模型的比较
3. select 函数
该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。
调用 select 告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等待多长时间。
#include <sys/select.h>
#include <sys/time.h>
//返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
}
timeout参数有三种可能:
1. 永远等待下去:仅在有一个描述符准备好I/O时才返回。置为空指针。
2. 等待一段固定时间:在有一个描述符准备好I/O时返回。
3. 根本不等待:检查描述符后立即返回,称为轮询(polling)。
尽管timeval结构允许我们指定了一个微秒级的分辨率,然而内核支持的真实分辨率往往粗糙的多。
中间的三个参数 readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述符。
目前支持的异常条件只有两个:
1. 某个套接字的带外数据到达。
2. 某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息。
select使用描述符集,通常是一个整数数组,其中每个整数中的每一位对应一个描述符。例如,假设使用32位整数,那么该数组的第一个元素对应于描述符0~31,第二个元素对应于描述符32!63 .
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); /* trun off the bit for fd in fdset */
int FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset? */
select函数的的中间三个参数readset、writeset和exceptset中,如果我们对某一个的条件不感兴趣,就可以把它设为空指针。
maxfdp1参数指定待测试的描述符个数,它的值是待测试的最大描述符加 1 .
例:
fd_set rset;
FD_ZERO(&rset);
FD_SET(1, &rset);
FD_SET(4, &rset);
FD_SET(5, &rset);
maxfdp1值就是 6 .
select函数修改由指针readset、writeset和exceptset所指向的描述符集,因而这三个参数都是值-结果参数。
描述符集内任何与未就绪描述符对应的位返回时均清成0.
3.1 描述符就绪条件
满足下列四个条件中的任何一个时,一个套接字准备好读。
- 该套接字接受缓冲区中的数据字节数大于等于套接字接受缓冲区低水位标记的当前大小。
- 该连接的读半部关闭。
- 该套接字是一个监听套接字且已完成的连接数不为 0 .
- 其上有一个套接字错误待处理。
下列四个条件中的任何一个满足时,一个套接字准备好写
- 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小,并且或者该套接字已连接,或者该套接字不需要连接。
- 该连接的写半部关闭。
- 使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终。
- 其上有一个套接字错误待处理。
4. shutdown 函数
终止网络连接的通常方法是调用close函数。
close函数有两个限制,却可以使用shutdown函数来避免。
1. close把描述符的引用计数减 1 ,仅在该计数变为 0 时才关闭套接字。使用shutdown可以不管引用计数就激发TCP的正常连接终止序列。
2. close终止读和写两个方向的数据传送。
#include <sys/socket.h>
//返回:若成功则为0;若出错则为-1
int shutdown(int sockfd, int howto);
函数的行为依赖于howto的值:
SHUT_RD 关闭连接的读这一半–套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。
SHUT_WR 关闭连接的写这一半–对于TCP套接字,这称为半关闭。
SHUT_RDWR 连接的读半部和写半部都关闭–这与调用shutdown两次等效。
5. 拒绝服务型攻击
当一个服务器在处理多个客户时,它绝对不能阻塞于只与单个客户相关的某个函数调用。否则可能导致服务器被挂起,拒绝为所有其它客户提供服务。这就是拒绝服务型攻击。
可能的解决办法包括:
1. 使用非阻塞式I/O 。
2. 让每个客户由单独的控制线程提供服务。
3. 对I/O操作设置一个超时。
6. pselect 函数
pselect 函数是由POSIX发明的。
#include <sys/select.h>
#include <signal.h>
#include <time.h>
//返回:若有就绪描述符则为其数目,若超时则为 0, 若出错则为 -1
int pselect(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timespec *timeout, const sigset_t *sigmask);
pselect 相对于select有两个变化:
1. pselect使用timespec结构,而不使用timeval结构。
struct timesepc
{
time_t tv_spec; /* seconds */
long tv_nsec; /* nanoseconds */
}
新结构的 tv_nsec 指定纳秒数,而旧结构的改成员tv_usec指定指定微秒数。
2. 增加了第 6 个参数。一个指定信号掩码的指针。该参数允许程序先禁止递交某些信号,再测试由这些当前被禁止信号的信号处理函数设置的全局变量,然后调用pselect,告诉它重新设置信号掩码。
7. poll 函数
poll 提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息。
#include <poll.h>
//返回:若有就绪描述符则为其数目,若超时则为0,若出错则为 -1
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
第一个参数是指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。
struct pollfd
{
int fd; /* descriptor to check */
short events; /* events of interest on fd */
short revents; /* events that occurred on fd */
}
要测试的条件由events成员指定,函数在相应的revents成员中返回该描述符的状态,每个描述符都有两个变量,一个为调用值,一个为返回结果。