什么是I/O复用

在客户阻塞(例如标准输入)的时候,服务器进程被杀死.这时候服务器进程虽然正确的给客户端发送了一个FIN,但是客户进程正阻塞在标准输入的过程无法读到这个EOF。这样的进程需要一种预先告知内核的能力,使内核一旦发现进程准备的一个或多个I/O准备就绪就通知进程. 这种能力被称为I/O复用

常见的I/O 模型

  1. 阻塞式I/O模型
  2. 非阻塞式I/O模型 (轮询访问)
  3. I/O复用
  4. 信号驱动式I/O(SIGIO) (例如 SIGIO)
  5. 异步I/O

I/O复用

select/poll/epoll 三种模型都是I/O复用模型。

IO复用模型.jpg

信号驱动I/O 模型

利用SIGIO 信号实现

信号驱动IO.jpg

异步I/O

异步IO.jpg

区别

IO模型区别.jpg

Select模型

select模型:将所有发生请求的客户端(建议1024以下)进行监听,采用的是轮询模型。
int select(int nfds,fd_set readfds,fd_set writefds,fd_set *exceptfds,struct timeval *timeout);

描述:调用此函数监听文件描述符集合中是否有读写请求,如果有,将传入参数进行置位再传出。需要进行轮询操作(while),才能达到实时监听的效果

nfds:所监听的文件描述符中,最大的文件描述符+1

readfds:监控有读数据到达文件描述符集合,传入传出参数

writefds:监控写数据到达文件描述符集合,传入传出参数

exceptfds:监控异常发生到达文件描述符集合,如带外数据到达异常,传入传出参数

timeout:定时阻塞监控事件,3中情况:

  1. NULL。永远等下去
  2. 设置timeval,等待固定时间
  3. 设置timeval里时间均为0.检查描述字后立即返回,轮询

返回值:监听集合中满足条件的总数。失败返回-1

理解

理解select模型的关键在于理解fd_set类型,这个类型就是多个整型字的集合,每个bit代表一个文件描述符

当select函数执行后,系统会重新设置fd_set中的内容,接下来会对有响应的文件描述符置位.(意味着我们每一次调用select 的时候都需要将fd集合拷贝到内核态然后内核对里面进行修改.)

select.jpg

select缺点:
(1)每次调⽤用select,都需要把fd集合从⽤用户态拷贝到内核态,这个开销在fd很多时会很⼤大
(2)同时每次调⽤用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很⼤大
(3)select⽀支持的⽂文件描述符数量太⼩小了,默认是1024

代码

/* server.c */
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
# include <netinet/in.h>   
# include "wrap.h"
# define MAXLINE 80
# define SERV_PORT 8000
int main(int argc, char *argv[])
{
	int i, maxi, maxfd, listenfd, connfd, sockfd;
	int nready, client[FD_SETSIZE]; /* FD_SETSIZE 默认为 1024 */
	ssize_t n;
	fd_set rset, allset;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN]; /* # define INET_ADDRSTRLEN 16 */
	socklen_t cliaddr_len;
	struct sockaddr_in cliaddr, servaddr;
	listenfd = Socket(AF_INET, SOCK_STREAM, 0);
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(SERV_PORT);
	bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
	listen(listenfd, 20); /* 默认最大128 */
	maxfd = listenfd; /* 初始化 */
	maxi = -1; /* client[]的下标 */
	for (i = 0; i < FD_SETSIZE; i++)
		client[i] = -1; /* 用-1初始化client[] */
	FD_ZERO(&allset);
	FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */
	for ( ; ; ) {
		rset = allset; /* 每次循环时都从新设置select监控信号集 */
		nready = select(maxfd+1, &rset, NULL, NULL, NULL);
		if (nready < 0)
			perr_exit("select error"); 
		if (FD_ISSET(listenfd, &rset)) { /* new client connection */
			cliaddr_len = sizeof(cliaddr);
			connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
			printf("received from %s at PORT %d\n",
			inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
			ntohs(cliaddr.sin_port));
			for (i = 0; i < FD_SETSIZE; i++)
				if (client[i] < 0) {
					client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */
					break;
				}
			/* 达到select能监控的文件个数上限 1024 */
			if (i == FD_SETSIZE) {
				fputs("too many clients\n", stderr);
				exit(1);
			}
			FD_SET(connfd, &allset); /* 添加一个新的文件描述符到监控信号集里 */
			if (connfd > maxfd)
				maxfd = connfd; /* select第一个参数需要 */
			if (i > maxi)
				maxi = i; /* 更新client[]最大下标值 */
			if (--nready == 0)
				continue; /* 如果没有更多的就绪文件描述符继续回到上面select阻塞监听,负责处理未
			处理完的就绪文件描述符 */
		}
		for (i = 0; i <= maxi; i++) { /* 检测哪个clients 有数据就绪 */
			if ( (sockfd = client[i]) < 0)
			continue;
			if (FD_ISSET(sockfd, &rset)) {
				if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
				/* 当client关闭链接时,服务器端也关闭对应链接 */
				Close(sockfd);
				FD_CLR(sockfd, &allset); /* 解除select监控此文件描述符 */
				client[i] = -1;
				} else {
					int j;
					for (j = 0; j < n; j++)
						buf[j] = toupper(buf[j]);
					Write(sockfd, buf, n);
				}
			if (--nready == 0)
			break;
			}
		}
	}
	close(listenfd);
	return 0;
}