第6章 I/O复用 select 和 poll 函数
I/O 复用的能力: 如果一个或多个 I/O 条件满足(例如,输入已准备好被读,或者描述字可以承接更多的输出)时,我们就被通知到。
I/O 复用由函数 select 和 poll 支持。
I/O 复用典型的应用场合:
a. 当客户处理多个描述字(一般是交互式输入和网络套接口),必须使用 I/O 复用;
b. 一个客户同时处理多个套接口[较少出现];
c. TCP 服务器既要处理监听套接口,又要处理已接套接口;
d. 服务器既要处理 TCP,又要处理 UDP;
e. 服务器要处理多个服务或多个协议[例如inetd守护进程]。
Unix 下五个 I/O 模型:
1. 阻塞的 I/O 模型
最流行的 I/O 模型,缺省时,所有套接口都是阻塞的。
以系统调用 recvfrom 为例,我们说进程阻塞的整段时间是指从调用 recvfrom 开始到它返回的这段时间,当进程返回成功指示时,应用进程开始处理。
2. 非阻塞的 I/O 模型
当我们把一个套接口设置成非阻塞方式时,即通知内核: 当请求的 I/O操作非得让进程睡眠不能完成时,不要让进程睡眠,而应返回一个错误。
当一个应用程序对一个非阻塞描述套接字循环调用 recvfrom 时,我们称此过程为轮询 polling。
3. I/O 复用模型
4. 信号驱动 I/O 模型
使用信号,让内核在描述字准备好时用信号 SIGIO 通知我们 —— 信号驱动 I/O。
5. 异步 I/O 模型
Posix.1 的1993版本中的新内容。
select函数
这个函数允许进程指示内核等待多个事件中的任一个发生,并仅在一个或多个事件发生或经过某指定的时间后才唤醒进程。
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1, fd_set *readset, fd_set *writeset, df_set* exceptset, const struct timeval *timeout);
返回: 正确 -> 准备好描述字的正数目, 超时 -> 0, 出错 -> -1。
timeout: 告诉内核等待一组指定的描述字中的任一个准备好可花多少时间,结构 timeval 指定了秒数和微秒书成员。
struct timeval{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
有三种可能:
1. 永远等待下去: 仅在有一个描述字准备好 I/O 时才返回,为此,参数 timeout 设置为空指针;
2. 等待固定时间: 在一个描述字准备好 I/O 时返回,但不超过由 timeout 参数所指定的时间;
3. 根本不等待: 检查描述字后立即返回,这称为轮询 polling。为此,timeout 成员设置为0。
readset, writeset, exceptset 指定我们要让内核测试读,写和异常条件所需的描述字。
现在只支持两个异常条件:
1. 套接口带外数据到达;
2. 控制状态信息的存在,可从一个已置为分组方式的伪终端主端读到。
如何为 readset, writeset, exceptset 三个参数的每一个指定一个或多个描述字值?
select 使用描述字集,它一般是一个整数数组,每个数中的每一位对应一个描述字。例如,用32位整数,则数组的第一个元素对应于描述字 0~31,数组中的第二个元素对应于描述字 32~63,以此类推。[可能的实现方式之一]
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 */
void FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset */
参数 maxfdp1 指定被测试的描述字个数,它的值是要被测试的最大描述字加 1,因为描述字是从0开始。
参数 maxfdp1 之所以存在,完全是为了从效率考虑。
函数 select 修改由指针 readset, writeset, exceptset 所指的描述字集。这三个参数均为值-结果参数。当我们调用函数时,指定我们所关心的描述字集,当返回时,结果指示那些描述字已准备好。
描述字集合中任何与没有准备好的描述字相对应的位返回时清成0。
描述字在什么情况下准备好?
1. 下列四个条件中的任何一个满足时,套接口准备好读:
a. 套接口接收缓冲区中的数据字节数大于等于套接口接收缓冲区低潮限度的当前值。对这样的套接口的读操作将不阻塞并返回一个大于0的值(即准备好读入的数据量)。我们可以用套接口选项 SO_RCVLOWAT 来设置此低潮限度。对于 TCP 和 UDP 套接口,其缺省值为1。
b. 连接的读另一半关闭(也就是说接收了 FIN 的 TCP 连接)。对这样的套接口的读操作将不阻塞且返回0(即文件结束符)。
c. 套接口是一个监听套接口且已完成的连接数为非 0。
d. 有一个套接口错误待处理。对这样的套接口 的读操作将不阻塞且返回一个错误 (-1),errno 则被设置成明确的错误条件。这些待处理的错误(pending errors)也可以通过指定套接口选项 SO_ERROR 调用 getsockopt 来取得清除。
些列三个条件中的任一个满足时,套接口准备好写:
a. 套接口发送缓冲区中的可用空间字节数大于等于套接口发送缓冲区低潮限度的当前值。 对于 TCP 和 UDP 套接口,其缺省值一般为 2048。
b. 连接的写这一半关闭。对这样的套接口的写操作将产生信号 SIGPIPE。
c. 有一个套接口错误待处理。对这样的套接口的写操作将不阻塞且返回一个错误 (-1),errno 则设置成明确的错误条件。这些待处理的错误也可通过指定套接口选项 SO_ERROR 调用 getsockopt 来取得并清除。
如果一个套接口存在带外数据或者仍处于带外标记,那它有异常条件待处理。
函数 fileno 把标准 I/O 文件指针转换为其对应的描述字。例:
fileno(stdin) = 0, fileno(stdout) = 1, fileno(stderr) = 2
Ping 测量是针对长度为 84 字节的 IP 数据报进行。
shutdown 函数
终止网络连接的正常方法是调用 close, 但 close 有两个限制可由函数 shutdown 来避免。
1. close 将描述字的访问计数减 1,仅在此计数为 0 时才关闭套接口。用 shutdown 我们可以激发 TCP 的正常连接终止序列(由 FIN 开始的四个分节),而不管访问计数。
2. close 终止了数据传送的两个方向: 读和写。 由于TCP连接是全双工的,很多时候我们要通知另一端我们已完成了数据发送,即使那一端仍有许多数据要发送也是如此。
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
返回: OK -> 0, Error -> -1。
此函数功能依赖 howto 参数的值:
SHUT_RD
关闭连接的读这一半,不再接受套接口中的数据且现留的套接口接受缓冲区中的数据都作废。进程不能再对套接口这行任何读函数。调用此函数后,由TCP套接口接收的任何数据都被确认,但数据本身扔掉。
SHUT_WR
关闭连接的写这一半,在TCP场合下,这称为半关闭(half-close)。当前留在套接口发送缓冲区中的数据都被发送,后跟正常的TCP连接终止序列。进程不能再执行套接口的任何写函数。
SHUT_RDWR
连接的读这一半和写这一半都关闭。等效与先后调用 SHUT_RD 和 SHUT_WR。
当一个服务器正在处理多个客户时,服务器决不能阻塞于只与单个客户相关的函数调用。如果这样的话,服务器将悬挂并拒绝为其他客户提供服务,这称为拒绝服务 denial of service 型攻击。
拒绝服务型攻击的处理办法:
a. 使用非阻塞 I/O 模型;
b. 让每个客户由单独的控制线程提供服务;
c. 对I/O 操作设置超时。
函数pselect
由 Posix.1g 发明
#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 sygset_t *sigmask);
返回: OK -> 准备好扫描的个数, 超时 -> 0, 出错 -> -1。
struct timespec {
time_t tv_sec; /* seconds 秒*/
logn tv_nsec; /* nanoseconds 纳秒 */
};
sigmask: 指向信号掩码的指针。
poll 函数
poll 提供了与 select 相似的功能,但当涉及到流设备时,它还提供附加信息。
#include <poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
返回: OK -> 准备好描述字的个数, 超时 -> 0, 出错 -> -1
struct pollfd{
int fd; /* descriptor to check */
short events; /* events of interest on fd */
short revents; /* events that occurred on fd */
};
要测试的条件由成员 events 规定,函数在相应的 revents 成员中返回描述字的状态。(每个描述字有两个变量,一个为调用值,另一个为结果,以此避免使用值-结果参数)。
ndfs: 数组 fdarray 中元素的个数。
timeout: 函数返回前等待多长时间。有以下三种情况:
INFTIM: 永远等待; 0: 立即返回,不阻塞; > 0 : 等待指定数目的毫秒数。
如果不关心某个特定描述字,可将 pollfd 结构的 fd 成员置为负值,这样就可忽略成员 events,且返回时将成员 revents 的值置为 0。
I/O 复用由函数 select 和 poll 支持。
I/O 复用典型的应用场合:
a. 当客户处理多个描述字(一般是交互式输入和网络套接口),必须使用 I/O 复用;
b. 一个客户同时处理多个套接口[较少出现];
c. TCP 服务器既要处理监听套接口,又要处理已接套接口;
d. 服务器既要处理 TCP,又要处理 UDP;
e. 服务器要处理多个服务或多个协议[例如inetd守护进程]。
Unix 下五个 I/O 模型:
1. 阻塞的 I/O 模型
最流行的 I/O 模型,缺省时,所有套接口都是阻塞的。
以系统调用 recvfrom 为例,我们说进程阻塞的整段时间是指从调用 recvfrom 开始到它返回的这段时间,当进程返回成功指示时,应用进程开始处理。
2. 非阻塞的 I/O 模型
当我们把一个套接口设置成非阻塞方式时,即通知内核: 当请求的 I/O操作非得让进程睡眠不能完成时,不要让进程睡眠,而应返回一个错误。
当一个应用程序对一个非阻塞描述套接字循环调用 recvfrom 时,我们称此过程为轮询 polling。
3. I/O 复用模型
4. 信号驱动 I/O 模型
使用信号,让内核在描述字准备好时用信号 SIGIO 通知我们 —— 信号驱动 I/O。
5. 异步 I/O 模型
Posix.1 的1993版本中的新内容。
select函数
这个函数允许进程指示内核等待多个事件中的任一个发生,并仅在一个或多个事件发生或经过某指定的时间后才唤醒进程。
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1, fd_set *readset, fd_set *writeset, df_set* exceptset, const struct timeval *timeout);
返回: 正确 -> 准备好描述字的正数目, 超时 -> 0, 出错 -> -1。
timeout: 告诉内核等待一组指定的描述字中的任一个准备好可花多少时间,结构 timeval 指定了秒数和微秒书成员。
struct timeval{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
有三种可能:
1. 永远等待下去: 仅在有一个描述字准备好 I/O 时才返回,为此,参数 timeout 设置为空指针;
2. 等待固定时间: 在一个描述字准备好 I/O 时返回,但不超过由 timeout 参数所指定的时间;
3. 根本不等待: 检查描述字后立即返回,这称为轮询 polling。为此,timeout 成员设置为0。
readset, writeset, exceptset 指定我们要让内核测试读,写和异常条件所需的描述字。
现在只支持两个异常条件:
1. 套接口带外数据到达;
2. 控制状态信息的存在,可从一个已置为分组方式的伪终端主端读到。
如何为 readset, writeset, exceptset 三个参数的每一个指定一个或多个描述字值?
select 使用描述字集,它一般是一个整数数组,每个数中的每一位对应一个描述字。例如,用32位整数,则数组的第一个元素对应于描述字 0~31,数组中的第二个元素对应于描述字 32~63,以此类推。[可能的实现方式之一]
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 */
void FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset */
参数 maxfdp1 指定被测试的描述字个数,它的值是要被测试的最大描述字加 1,因为描述字是从0开始。
参数 maxfdp1 之所以存在,完全是为了从效率考虑。
函数 select 修改由指针 readset, writeset, exceptset 所指的描述字集。这三个参数均为值-结果参数。当我们调用函数时,指定我们所关心的描述字集,当返回时,结果指示那些描述字已准备好。
描述字集合中任何与没有准备好的描述字相对应的位返回时清成0。
描述字在什么情况下准备好?
1. 下列四个条件中的任何一个满足时,套接口准备好读:
a. 套接口接收缓冲区中的数据字节数大于等于套接口接收缓冲区低潮限度的当前值。对这样的套接口的读操作将不阻塞并返回一个大于0的值(即准备好读入的数据量)。我们可以用套接口选项 SO_RCVLOWAT 来设置此低潮限度。对于 TCP 和 UDP 套接口,其缺省值为1。
b. 连接的读另一半关闭(也就是说接收了 FIN 的 TCP 连接)。对这样的套接口的读操作将不阻塞且返回0(即文件结束符)。
c. 套接口是一个监听套接口且已完成的连接数为非 0。
d. 有一个套接口错误待处理。对这样的套接口 的读操作将不阻塞且返回一个错误 (-1),errno 则被设置成明确的错误条件。这些待处理的错误(pending errors)也可以通过指定套接口选项 SO_ERROR 调用 getsockopt 来取得清除。
些列三个条件中的任一个满足时,套接口准备好写:
a. 套接口发送缓冲区中的可用空间字节数大于等于套接口发送缓冲区低潮限度的当前值。 对于 TCP 和 UDP 套接口,其缺省值一般为 2048。
b. 连接的写这一半关闭。对这样的套接口的写操作将产生信号 SIGPIPE。
c. 有一个套接口错误待处理。对这样的套接口的写操作将不阻塞且返回一个错误 (-1),errno 则设置成明确的错误条件。这些待处理的错误也可通过指定套接口选项 SO_ERROR 调用 getsockopt 来取得并清除。
如果一个套接口存在带外数据或者仍处于带外标记,那它有异常条件待处理。
函数 fileno 把标准 I/O 文件指针转换为其对应的描述字。例:
fileno(stdin) = 0, fileno(stdout) = 1, fileno(stderr) = 2
Ping 测量是针对长度为 84 字节的 IP 数据报进行。
shutdown 函数
终止网络连接的正常方法是调用 close, 但 close 有两个限制可由函数 shutdown 来避免。
1. close 将描述字的访问计数减 1,仅在此计数为 0 时才关闭套接口。用 shutdown 我们可以激发 TCP 的正常连接终止序列(由 FIN 开始的四个分节),而不管访问计数。
2. close 终止了数据传送的两个方向: 读和写。 由于TCP连接是全双工的,很多时候我们要通知另一端我们已完成了数据发送,即使那一端仍有许多数据要发送也是如此。
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
返回: OK -> 0, Error -> -1。
此函数功能依赖 howto 参数的值:
SHUT_RD
关闭连接的读这一半,不再接受套接口中的数据且现留的套接口接受缓冲区中的数据都作废。进程不能再对套接口这行任何读函数。调用此函数后,由TCP套接口接收的任何数据都被确认,但数据本身扔掉。
SHUT_WR
关闭连接的写这一半,在TCP场合下,这称为半关闭(half-close)。当前留在套接口发送缓冲区中的数据都被发送,后跟正常的TCP连接终止序列。进程不能再执行套接口的任何写函数。
SHUT_RDWR
连接的读这一半和写这一半都关闭。等效与先后调用 SHUT_RD 和 SHUT_WR。
当一个服务器正在处理多个客户时,服务器决不能阻塞于只与单个客户相关的函数调用。如果这样的话,服务器将悬挂并拒绝为其他客户提供服务,这称为拒绝服务 denial of service 型攻击。
拒绝服务型攻击的处理办法:
a. 使用非阻塞 I/O 模型;
b. 让每个客户由单独的控制线程提供服务;
c. 对I/O 操作设置超时。
函数pselect
由 Posix.1g 发明
#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 sygset_t *sigmask);
返回: OK -> 准备好扫描的个数, 超时 -> 0, 出错 -> -1。
struct timespec {
time_t tv_sec; /* seconds 秒*/
logn tv_nsec; /* nanoseconds 纳秒 */
};
sigmask: 指向信号掩码的指针。
poll 函数
poll 提供了与 select 相似的功能,但当涉及到流设备时,它还提供附加信息。
#include <poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
返回: OK -> 准备好描述字的个数, 超时 -> 0, 出错 -> -1
struct pollfd{
int fd; /* descriptor to check */
short events; /* events of interest on fd */
short revents; /* events that occurred on fd */
};
要测试的条件由成员 events 规定,函数在相应的 revents 成员中返回描述字的状态。(每个描述字有两个变量,一个为调用值,另一个为结果,以此避免使用值-结果参数)。
ndfs: 数组 fdarray 中元素的个数。
timeout: 函数返回前等待多长时间。有以下三种情况:
INFTIM: 永远等待; 0: 立即返回,不阻塞; > 0 : 等待指定数目的毫秒数。
如果不关心某个特定描述字,可将 pollfd 结构的 fd 成员置为负值,这样就可忽略成员 events,且返回时将成员 revents 的值置为 0。