I/O复用使用场景:
-当客户处理多个描述符(通常是交互式输入和网络套接字)时,必须使用I/O复用。
-一个客户同时处理多个套接字是可能的,不过比较少见。
-如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般就要使用I/O复用。
-如果一个TCP服务器既要处理TCP又要处理UDP,一般就要使用I/O复用。
-如果一个服务器要处理多个服务或者多个协议,一般就要使用I/O复用。
I/O模型
5种模型:
-阻塞式I/O
-非阻塞式I/O
-I/O复用(select 和 poll)
-信号驱动式I/O
-异步I/O
一个输入操作通常包含两个不同的阶段:
-等待数据准备好
-从内核向进程复制数据
对于套接字上的输入操作:第一步等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核数据区复制到应用进程缓冲区。
1.阻塞式I/O模型
最流行的I/O模型就是阻塞式I/O模型,默认情况下,所有套接字都是阻塞的。
以数据报套接字(UDP)为例
进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。
2.非阻塞式I/O模型
进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。
前三次调用recvfrom时没有数据可返回,因此内核转而立即返回一个错误。第四次调用recvfrom时已有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成功返回。
当一个应用程序对一个非阻塞描述符循环调用recvfrom时,我们称之为轮询(polling)。应用进程持续轮询内核,以查看某个操作是否就绪。
3.I/O复用模型
调用select和poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的I/O系统调用上。
阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,调用recvfrom把所读数据报复制到应用进程缓冲区。
使用select的优势在于我们可以等待多个描述符就绪。
4.信号驱动式I/O模型
内核在描述符就绪时发送SIGIO信号通知,称这种模型为信号驱动式I/O.
当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它读取数据报。
无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行。
5.异步I/O模型
异步I/O由POSIX规范定义。工作机制是:告知内核启动莫格操作,并让内核在整个操作完成后通知我们。
与信号驱动式的区别在于:信号驱动式由内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。
调用aio_read函数给内核传递描述符,缓冲区指针,缓冲区大小和文件偏移,并告诉内核当整个操作完成时如何通知我们。该系统调用立即返回,而且在等待I/O完成期间,我们的进程不被阻塞。假设要求内核在操作完成时产生某个信号。该信号直到数据已复制到应用进程缓冲区才产生。
同步I/O和异步I/O比较
同步I/O操作导致请求进程阻塞,直到I/O操作完成。
异步I/O操作不导致请求进程阻塞。
前四种模式-阻塞式I/O模型,非阻塞式I/O模型,I/O复用模型和信号驱动式I/O模型都是同步I/O,因为其中真正的I/O操作(recvfrom)将阻塞进程。
select函数
该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。
#include<sys/select.h>
#include<sys/time.h>
int select (int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
//若有就绪描述符则返回其数目,超时返回0,出错返回-1.
最后一个参数timeout告诉内核等待所指定描述符中的任意一个就绪可花多长事件。timeval结构体指定这段时间的秒数和微秒数。
struct timeval {
long tv_sec; //seconds
long tv_usec; //microseconds
};
这个参数有3种可能:
1.永远等待:仅在有一个描述符准备好I/O时才返回。把该参数设置为空指针。
2.等待一段固定事件:在有一个描述符准备好I/O时返回。但是不超过由该参数指定的时间。
3.根本不等待:检查描述符后立即返回,这称为轮询。为次,该参数执行一个timeval结构体,而且其中的定 时器值为0.
中间的三个参数readset,writeset,exceptset指定我们要让内核测试读,写和异常条件的描述符。
目前支持的异常条件只有两个:
-某个套接字的带外数据(OOB)的到达。
-某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息。
select使用描述符集,通常是一个整数数组,其中每个整数中的每一位对应一个描述符。
四个宏:
分配一个fd_set数据类型的描述符集,并用这些宏设置或测试该集合中的每一位。
举例:以下代码用于定义一个fd_set类型的变量,然后打开描述符1,4,5的对应位。
fd_set rset;
FD_ZERO(&rset); //清除所有位为0
FD_SET(1,&rset); //打开第一位
FD_SET(4,&rset); //打开第四位
FD_SET(5,&rset); //打开第五位
描述符集的初始化非常重要。
select函数的三个参数readset,writeset,exceptset中,如果对某一个的条件不感兴趣,就可以把它设为空指针。
maxfdpl参数指定待测试的描述符个数,它的值是待测试的最大描述符加1(描述符0,1,2...一直到maxfdpl-1 均被测试)。
select函数修改由指针readset,writeset和exceptset所指向的描述符集,因而这三个参数都是值-结果参数。调用该函数时,指定所关心的描述符的值,函数返回时,结果将指示哪些描述符已就绪。用FD_ISSET宏来测试fd_set数据类型中的描述符,描述符集内任何未就绪描述符对应的位返回时清0。因此,每次重新调用select函数时,都得再次把所有描述符集内所关心的为均置为1。
描述符就绪条件
str_cli函数修订版
str_cli函数完成客户处理循环:从标准输入读入一行文本,写到服务器上,读回服务器对该行的回射,并把回射行写到标准输出上。
#include "unp.h"
#include "myerr.h"
void str_cli(FILE *fp, int sockfd)
{
int maxfdp1;
fd_set rset;
char sendline[MAXLINE],recvline[MAXLINE];
FD_ZERO(&rset);
while(1) {
FD_SET(fileno(fp),&rset);
FD_SET(sockfd,&rset);
maxfdp1 = max(fileno(fp),sockfd) +1;
select(maxfdp1, &rset, NULL,NULL,NULL);
if (FD_ISSET(sockfd,&rset)) { //sockfd is readable
if (read(sockfd, recvline, MAXLINE) == 0)
err_quit("str_cli: server terminated prematurely");
fputs(recvline,stdout);
}
if (FD_ISSET(fileno(fp), &rset)) { //input is readable
if (fgets(sendline, MAXLINE, fp) == NULL)
return ;
write(sockfd, sendline, strlen(sendline));
}
}
shutdown函数
close把描述符的引用计数减1,仅在该计数变为0时关闭套接字。
close终止读和写两个方向的数据传送。TCP连接是全双工的,有时候需要告知对端我们已经完成了数据发送,即使对端仍有数据要发送给我们。
int shutdown(int sockfd, int howto);
howto参数:
SHUT_RD,关闭连接的读这一半——套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。
SHUT_WR,关闭连接的写这一半——对于TCP为套接字,这称为半关闭。当前留在套接字发送缓冲区中的数据将被发送掉,后跟TCP的正常连接终止序列。
SHUT_RDWR,连接的读半部和写半部都关闭
poll函数
#include<poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
//若有描述符就绪则返回其数目,若超时则为0,若出错则为-1
第一个参数是指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。
struct pollfd {
int fd;
short events;
short revents;
}
要测试的条件由events成员指定,函数在相应的revents成员中返回该描述符的状态。这两个成员中的每一个都由指定某个特定条件的一位或多位构成。
第一部分是处理输入的四个常值,第二个部分是处理输出的三个常值,第三部分是处理错误的三个常值,不能在events中设置,只能在revents中返回。
poll时别三类数据:普通,优先级带,高优先级。
timeout参数指定poll函数返回前等待多长时间。它是一个指定应等待毫秒数的正值。