一、五种I/O模型
1、阻塞I/O模型
最流行的I/O模型是阻塞I/O模型,缺省情形下,所有套接口都是阻塞的。我们以数据报套接口为例来讲解此模型(我们使用UDP而不是TCP作为例子的原因在于就UDP而言,数据准备好读取的概念比较简单:要么整个数据报已经收到,要么还没有。然而对于TCP来说,诸如套接口低潮标记等额外变量开始活动,导致这个概念变得复杂)。
进程调用recvfrom,其系统调用直到数据报到达且被拷贝到应用进程的缓冲区中或者发生错误才返回,期间一直在等待。我们就说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的。
2、非阻塞I/O模型
进程把一个套接口设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。也就是说当数据没有到达时并不等待,而是以一个错误返回。
3、I/O复用模型
调用select或poll,在这两个系统调用中的某一个上阻塞,而不是阻塞于真正I/O系统调用。 阻塞于select调用,等待数据报套接口可读。当select返回套接口可读条件时,调用recevfrom将数据报拷贝到应用缓冲区中。
4、信号驱动I/O模型
首先开启套接口信号驱动I/O功能, 并通过系统调用sigaction安装一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据报准备好被读时,就为该进程生成一个SIGIO信号。随即可以在信号处理程序中调用recvfrom来读数据报,井通知主循环数据已准备好被处理中。也可以通知主循环,让它来读数据报。
5、异步I/O模型
告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核拷贝到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:
信号驱动I/O:由内核通知我们何时可以启动一个I/O操作,
异步I/O模型:由内核通知我们I/O操作何时完成。
二、I/O复用的典型应用场合:
1、当客户处理多个描述字(通常是交互式输入和网络套接口)时,必须使用I/O复用。
2、如果一个服务器要处理多个服务或者多个协议(例如既要处理TCP,又要处理UDP),一般就要使用I/O复用。
三、支持I/O复用的系统调用
目前支持I/O复用的系统调用有select、pselect、poll、epoll:
1、select函数
该函数允许进程指示内核等待多个事件中的任何一个发生,并仅在有一个或多个事件发生或经历一段指定的时间后才唤醒它。
格式为:
4 | int select( int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout); |
6 | 返回:就绪描述字的正数目,0-超时,-1-出错 |
我们从该函数的最后一个参数开始介绍,它告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。
struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds
};
这个参数有三种可能:
(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,我们把该参数设置为空指针。
(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。
中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果我们对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:
void FD_ZERO(fd_set *fdset); //清空集合
void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中
void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写 ?
目前支持的异常条件只有两个:
(1)某个套接口的带外数据的到达。
(2)某个已置为分组方式的伪终端存在可从其主端读取的控制状态信息。
第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(因此我们把该参数命名为maxfdp1),描述字0、1、2...maxfdp1-1均将被测试。
一个应用select的例子:
007 | #include <sys/socket.h> |
008 | #include <sys/types.h> |
009 | #include <netinet/in.h> |
013 | #include <sys/select.h> |
016 | #define SERVER_PORT 3333 //服务器端口号 |
018 | void str_cli( FILE *fp, int sockfd) |
020 | int maxfdp1, stdineof; |
031 | FD_SET(fileno(fp),&rset); |
032 | FD_SET(sockfd, &rset); |
034 | maxfdp1 = ((fileno(fp) > sockfd) ? fileno(fp) : sockfd) + 1; |
036 | select(maxfdp1, &rset, NULL, NULL, NULL); |
038 | if ( FD_ISSET(sockfd, &rset) ) |
040 | if ( (n = read(sockfd, buf, BUFSIZ)) == 0 ) |
044 | perror ( "server terminated prematurely" ); |
045 | write(fileno(stdout), buf, n); |
048 | if ( FD_ISSET(fileno(fp), &rset)) |
050 | if ( (n = read(fileno(fp), buf, BUFSIZ)) == 0 ) |
053 | shutdown(sockfd, SHUT_WR); |
054 | FD_CLR(fileno(fp), &rset); |
057 | write(sockfd, buf, n); |
062 | int main( int argc, char *argv[]) |
065 | struct sockaddr_in servaddr; |
071 | printf ( "Please input %s <hostname>\n" , argv[0]); |
076 | for (i = 0; i < 5; ++i) |
080 | if ( (sockfd[i] = socket(AF_INET, SOCK_STREAM,0)) < 0 ) |
082 | printf ( "Create socket error!\n" ); |
087 | bzero(&servaddr, sizeof (servaddr)); |
088 | servaddr.sin_family = AF_INET; |
089 | if ( (hp = gethostbyname(argv[1])) != NULL ) |
091 | bcopy(hp->h_addr, ( struct sockaddr*)&servaddr.sin_addr, hp->h_length); |
093 | else if (inet_aton(argv[1], &servaddr.sin_addr) < 0 ) |
095 | printf ( "Input Server IP error!\n" ); |
098 | servaddr.sin_port = htons(SERVER_PORT); |
101 | if ( connect(sockfd[i],( struct sockaddr*)&servaddr, sizeof (servaddr)) < 0 ) |
103 | printf ( "Connect server failure!\n" ); |
107 | str_cli(stdin, sockfd[0]); |
注:本章内容摘自<Unix 网络编程>第六章。
2、pselect函数
pselect函数是由POSIX发明的,如今许多Unix变种都支持它。
5 | 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结构。timespec结构是POSIX的又一个发明。
struct timespec{
time_t tv_sec; //seconds
long tv_nsec; //nanoseconds
};
这两个结构的区别在于第二个成员:新结构的该成员tv_nsec指定纳秒数,而旧结构的该成员tv_usec指定微秒数。
2、pselect函数增加了第六个参数:一个指向信号掩码的指针。该参数允许程序先禁止递交某些信号,再测试由这些当前被禁止的信号处理函数设置的全局变量,然后调用pselect,告诉它重新设置信号掩码。
关于第二点,考虑下面的例子,这个程序的SIGINT信号处理函数仅仅设置全局变量intr_flag并返回。如果我们的进程阻塞于select调用,那么从信号处理函数的返回将导致select返回EINTR错误。然而调用select时,代码看起来大体如下: