五种IO模型简介
阻塞式IO,非阻塞式IO,IO复用,信号驱动式IO,异步IO为五种UNIX\LINUX下的IO模型。
- 阻塞式IO。默认情况下所有套接字都为阻塞,当进程调用IO系统调用时(如read,recvfrom),会从用户态转换成内核态,准备从内核的缓存区中将数据复制到进程的缓冲区,如果数据没有准备好(即没有到达套接字),则一直阻塞直到数据准备好然后进行复制(或者发生错误返回)。
- 非阻塞式IO。进程设置为非阻塞式IO即通知内核,当没有数据准备好时候,不要把该进程阻塞,而是返回一个错误。然后进程会以论询的方式调用系统调用。
- IO复用模型。我们可以将一个系统调用(如select)与多个套接字(或描述符)关联,当任意一个描述符状态改变,select都会返回。
- 信号驱动式IO。就是当内核在描述符就绪的时候发出信号SIGIO信号,如果我们在进程中注册了SIGIO的信号处理函数,则可以在信号处理函数中读写套接字。优势在于等待信号到达这段时间进程不会被阻塞。
- 异步IO。前面四种IO模型都是属于同步IO(即会发生进程阻塞),异步IO则不会导致请求进程阻塞。首先告知内核进行某个操作(如读、写),然后让内核在整个操作结束后再通知进程,在内核运行过程中进程可以做其他的事情,不会阻塞。
IO复用模型之select
首先介绍select的工作原理:该函数是进程指示内核等待多个事件中任何一个发生,并只有一个或多个事件发生或经历一段指定时间后才能唤醒。遍历所有监督的描述符,找到事件发生的描述符,然后内核将数据复制到内存用户空间。
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1,fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
struct timeval{
long tv_sec;
long tv_usec;
}
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);
//timeout为内核在select阻塞等待的时间,若参数置空表示仅在有一个描述符准备好IO时才返回,否则一直等
// 中间三个参数分别指定让内核测试读、写、异常的描述符集合
//maxfdp1参数指定待测试的描述符个数(一般为最大描述符+1,因为描述符从0开始)
当select确定IO发生的时候,就会轮询所有fd,来检测是哪个描述符准备就绪,所以时间复杂度为O(n)。除此之外,单个进程所监视的文件描述符数量有限制,一般为FD_SETSIZE。
IO复用模型之poll
poll和select原理上类似,只不过使用结构数组来代表进程中每个描述符的状态变化。所以时间复杂度也是O(n)
#include <poll.h>
int poll(struct pollfd *fdarray , unsigned long nfds , int timeout);
//第一个参数结构数组,第二个参数结构数组中元素个数,第三个参数指定poll返回前等待时长
struct pollfd{
int fd;
short events; //指定测试条件
short revents; //返回描述符状态
}
对于poll来说,就不存在最大描述符限制的问题,因为其数组中元素大小是自己指定的。
IO复用模型之epoll
epoll与select和poll有很大的区别。首先,epoll使用一组函数完成任务,而不是单个函数,其次,epoll把用户关心的文件描述符上的事件放在内核的一个事件表中,从而无须select和poll那样每次都要重复传入文件描述符(或事件集)。但是,epoll需要一个额外的文件描述符来标识这个内核中的事件表。
#include <sys/epoll.h>
int epoll_create(int size);//size是给内核事件表的大小,函数返回的文件描述符即指定内核事件表
int epoll_ctl(int epfd, int op,int fd, struct epoll_event *event);
//epfd指内核事件表
//op指定操作类型 EPOLL_CTL_ADD(注册fd上事件) EPOLL_CTL_MOD(修改fd上事件) EPOLL_CTL_DEL(删除fd上的事件)
//fd参数即为要操作的文件描述符
//event指定事件
struct epoll_event
{
_uint32_t events;//epoll事件,EPOLLIN(数据可读)EPOLLOUT,EPOLLERR,EPOLLET,EPOLLONESHOT
epoll_data_t data;//用户数据
};
typedef union epoll_data
{
void* ptr;
int fd; //指定事件从属的目标描述符
unint32_t u32;
uint64_t u64;
}epoll_data_t;
int epoll_wait(int epfd, struct epoll_event* events, int maxevents,int timeout);
//函数成功时候返回就绪的文件描述符个数,失败时返回-1且设置errno
//函数如果检测到了事件则就会将所有的就绪事件从内核事件表中复制到events数组参数中
//maxevents指定最多监听多少个事件
//timeout即为超时值
LT和ET模式
epoll对文件描述符操作有两种ET(边缘触发)和LT(水平触发)。LT是默认的触发模式,当epoll_wait检测到LT模式下的文件描述符有事件发生,并通知应用程序,应用程序可以不立刻处理该事件,当应用程序下次调用epoll_wait时还会触发该事件,直到事件被处理。
对于ET模式,当epoll_wait检测到其上有事件发生并通知应用程序后,应用程序应该立刻处理该事件,因为后续的epoll_wait将不再向应用程序通知该事件。
#include "http_parse.h"
#define MAX_EVENT_NUMBER 1024
#define BUFFSIZE 10
int setnonblock(int fd){
int old_option=fcntl(fd,F_GETFL);
int new_option=old_option | O_NONBLOCK;
fcntl(fd,F_SETFL,new_option);
return old_option;
}
void addfd(int epollfd,int fd,bool enable_et){ //这个函数就是将fd加到epollfd所指向的内核事件表中
struct epoll_event epoll_e;
epoll_e.events=EPOLLIN;
epoll_e.data.fd=fd;
if(enable_et){
epoll_e.events|=EPOLLET;
}
int err= epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&epoll_e);
if(err==-1){
printf("epoll_ctl error\n");
}
setnonblock(fd);//每个设置为ET都是将fd设置为非阻塞
}
void use_LT(epoll_event *events,int number,int epollfd, int listenfd){
//epoll默认是水平触发
char buf[BUFFSIZE];
for(int i=0;i<number;i++){
int sockfd=events[i].data.fd;
if(listenfd==sockfd){//当有新连接的时候,通过fd判断
struct sockaddr_in client_addr;
socklen_t client_addr_len=sizeof(client_addr);
int clientfd=accept(listenfd,(struct sockaddr *)&client_addr,&client_addr_len);
addfd(epollfd,clientfd,false);
}
else if(events[i].events & EPOLLIN){//当有读取事件的时候,代码即触发,当一次没有读取完,可以第二次触发
printf("event target once\n");
memset(buf,'\0',BUFFSIZE);
int ret=recv(sockfd,buf,BUFFSIZE-1,0);
if(ret<=0){//如果读完或者异常
close(sockfd);
continue;
}
printf("get %d byte of content : %s\n",ret,buf);
}
else{
printf("something else happened\n");
}
}
}
void use_ET(epoll_event *events,int number,int epollfd, int listenfd){
//设置为边缘触发
char buf[BUFFSIZE];
for(int i=0;i<number;i++){
int sockfd=events[i].data.fd;
if(listenfd==sockfd){//当有新连接的时候,通过fd判断
struct sockaddr_in client_addr;
socklen_t client_addr_len=sizeof(client_addr);
int clientfd=accept(listenfd,(struct sockaddr *)&client_addr,&client_addr_len);
addfd(epollfd,clientfd,true);//对新连接的client开启边缘触发
}
else if(events[i].events & EPOLLIN){//当有读取事件的时候,代码即触发,但是边缘触发不会重复触发,所以要确保缓冲区中的数据完全读出
printf("event target once\n");
while(1){
memset(buf,'\0',BUFFSIZE);
int ret=recv(sockfd,buf,BUFFSIZE-1,0);//最大可以取BUFFSIZE-1,最后一个'\0'表示字符串结束
if(ret<0){
//如果读完
if(errno==EAGAIN || errno==EWOULDBLOCK){
printf("read later\n");
break;
}
close(sockfd);
break;
}
else if(ret==0){
close(sockfd);
}
else{
printf("get %d byte of content : %s\n",ret,buf);
}
}
}
else{
printf("something else happened\n");
}
}
}
int main(int argc,char *argv[]){
int listenfd;
struct sockaddr_in addr;
bzero(&addr,sizeof(addr));
addr.sin_family=AF_INET;
inet_pton(AF_INET,argv[1],&addr.sin_addr);
addr.sin_port=htons(atoi( argv[2]));
//inet_pton(AF_INET,argv[2],&addr.sin_port);
listenfd=socket(AF_INET,SOCK_STREAM,0);
assert(listenfd>=0);
int err=bind(listenfd,(struct sockaddr *)&addr,sizeof(addr));
assert(err != -1);
err=listen(listenfd,5);
assert(err!=-1);
epoll_event events[MAX_EVENT_NUMBER];
int epoll_fd=epoll_create(5);
assert(epoll_fd!=-1);
addfd(epoll_fd,listenfd,true);
//接下来分别用epoll的LT和ET来处理事件
while(1){
int ret=epoll_wait(epoll_fd,events,MAX_EVENT_NUMBER,-1);
if(ret<0){
printf("epoll wait error\n");
break;
}
//printf("get one\n");
//use_LT(events,ret,epoll_fd,listenfd);
use_ET(events,ret,epoll_fd,listenfd);
}
close(listenfd);
return 0;
}
三者区别
- 一个进程可以打开的最大连接描述符个数。select的最大连接个数由FD_SETSIZE宏定义确定;poll和select相似,但是其没有最大连接个数限制(由用户自己指定);epoll连接个数虽有上限,但是很大。
- 描述符个数增多后,效率影响。select每次调用都会对监控的描述符进行线性遍历,所以当描述符个数增多时,其性能会下降。poll和select相似。对于epoll来说,因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback(在内核事件表中),所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。更多见下表。
参考 《UNP》《Linux高性能服务器编程》