BIO和NIO详解两种高性能I/O设计模式的比较IO多路复用—由Redis的IO多路复用yinch

Redis的性能为什么如此之高?

Redis采用单线程架构I/O多路复用模型

redis客户端和服务端调用时,过程是发送命令、执行命令、返回结果三个过程。当多个客户端向redis取内容时,客户端命令的执行顺序是不确定的,但一定不会有多条命令被同时执行,不存在并发问题。

  • 纯内存访问

redis将所有数据存在内存,内存响应时间约为100纳秒

  • 非阻塞I/O

使用I/O多路复用模型

  • 单线程避免线程切换和竞态产生的消耗

IO block

考虑下面两种情况IO:
1.用系统调用read从socket里读数据
2.用系统调用read从磁盘文件中读取数据
上述两种情况,Linux只认为第一种是block的,因为Linux无法判断socket里有没有数据,如果对方没有发送数据,那么read就只能一直等数据来,Linux是可以感知到socket里没有数据,从而主动block。而第二种情况,有时可能读写磁盘时会慢,但是Linux无法预见这种情况,不会block。
基于这个设定,讨论IO时,要区分网络IO和文件IO。NIO和IO多路复用都是针对网络IO。

同步/异步
调用某个方法时,调用方必须等待其返回结果才能继续执行之后的代码
调用某个方法时,调用方直接得到被调用方回应,可以继续执行代码。而被调用方会将结果通过回调或事件通知等机制告知调用方。

阻塞/非阻塞
是指发起请求时一直等着调用结果,在结果返回前,当前线程会被挂起,只有等拿到结果后才执行和返回。
是指发起请求是调用结果即使未立即返回,当前线程也可以执行其他的事情。

BIO (Blocking I/O)
开启多个线程接收网络请求,一个线程处理一个网络请求。线程数随着并发连接数线性增长。

存在的问题:
1.线程增多,上下文切换增多,浪费CPU
2.占用很多内存

这样造成很多浪费,因为当没数据时线程只能阻塞,等着数据来,但实际上线程可以去做其他的事情,有数据后再来读取数据做一些事情就好了。

NIO(Non-blocking IO)
设置fd为非阻塞模式

fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK)

在BIO模式下,调用read,socket如果发现没数据到达,会Block住。
在NIO模式下,调用read,socket如果发现没数据到达,会立刻返回-1, 并且errno被设为EAGAIN。

NIO的思路是会一直轮询所有的fd,尝试read,如果有数据就处理,没数据就等一会再尝试读取数据。
存在的问题:
1.如有很多fd,那么要挨个read,造成大量上下文切换(read是系统调用,每调用一次就得在用户态和核心态切换一次)
2.间隔时间不容易掌控。

IO多路复用(Reactor)——异步阻塞IO

程序注册一些fd到事件分享器,事件分享器会监听这些fd是否有数据,如果有就通知对应的fd进行处理
linux提供的支持io多路复用的接口,select和poll

select

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

它接受3个文件描述符的数组,分别监听读取(readfds),写入(writefds)和异常(expectfds)事件。
代码:

While(1){
	if(select(fd+1,&read_fds, NULL, NULL, &tv) < 0){
		//出错
	}
	//遍历
	for(int I = 0 ;i <FD_SETSIZE;++i) {
		if (FD_ISSET(fd, &read_fds)) {
			if((newsockfd = accept (sockfd,&read_fds,sizeof(read_fds)))>=0) {
				 if ((nbytes = read(newsockfd , buf, sizeof(buf))) >= 0) {
				 	//处理数据
				 } else {
				    //出错
				 }	
			}
			else{
			//出错
			}	                 
		 }	
	}
}

select仅知道有io事件发生了,但无法知道是哪个fd,需要遍历数组找到对应的fd

缺点:
1.每次调用select,都需要把待监控的fd集合从用户态拷贝到内核态,当fd很大时,开销很大。
2.需要轮询所有fd,复杂度O(n)
3.支持最大fd数是1024

poll

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

与select本质一样,不同的是最大连接数无限制,因为改用链表而不是数组了

epoll
epoll会将哪个fd有数据通知到使用者,epoll实际上是事件驱动模式。复杂度为O(1)

//创建
epoll_create
//注册监听事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//等待事件发生
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);

优点:
1.线程安全
2.会返回有数据的fd,不需要遍历。

表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,因为epoll的通知机制需要很多函数回调。select和poll低效是因为每次它都需要轮询,但也不是不可以改善。

水平触发和边沿触发

水平触发只关心文件描述符中是否还有没完成处理的数据,如果有,不管怎样epoll_wait,总是会被返回。简单说——水平触发代表了一种“状态”。
边沿触发只关心文件描述符是否有新的事件产生,如果有,则返回;如果返回过一次,不管程序是否处理了,只要没有新的事件产生,epoll_wait不会再认为这个fd被“触发”了。简单说——边沿触发代表了一个“事件”。
那么边沿触发怎么才能迫使新事件产生呢?一般需要反复调用read/write这样的IO接口,直到得到了EAGAIN错误码,再去尝试epoll_wait才有可能得到下次事件。

AIO(Proactor)——异步非阻塞IO

异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。

具体Reactor和Proactor设计模式,可参考文章IO多路复用—由Redis的IO多路复用yinch