在看libevent模型之前,先对IO模型,主要是IO复用进行一个归纳总结和理解。
用户进程向系统发起read操作时,要在内核中进行数据准备和从内核到用户进程的拷贝(拷贝到用户进程用户进程才能使用)。此时,read操作才算完成,而后再进行处理。
我想请女神出去吃饭,于是给女神发微信我在楼下等你,期间什么也不干,就阻塞在楼下等待女神下来,然后去吃饭。
socket默认都是阻塞的。非阻塞则要将socket设置为NONBLOCK。非阻塞IO不同的是,每次read操作都会返回状态。可能准备好了,可能没有。因此用户进程需要不断发起read请求,去查询数据准备好了没有。可以理解为一个while循环,没有准备好,我就一直请求。
我想请女神吃饭,发了信息女神没下来,于是我就继续发,还没下来,继续。(女神生气了,嫌你烦,一直不下来,你就一直发,太耗电了,耗流量还。反应到编程中就是太耗CPU了。)直到女神下来,然后去吃饭。
上面的IO操作都是用户亲自去做的。IO复用的话,我们不亲自进行询问,我们委托一个函数帮我们去等。也就是委托专业的IO监测函数来干,比我们干的活多。
IO复用流程如上:
IO复用方式主要有三种:
A. select
int select(int maxfdp1,fd_set* readset, fd_set *writeset, fd_set* exceptset, const struct timeval * timeout);
首先看下select的函数原型,了解里面的参数:
maxfdp,待测试的描述符个数;
而后三个是读写和异常的描述字,都返回fd_set的指针。
fd_set是啥?我们来看一下linux给他的操作接口:
void FD_ZERO(fd_set *fdset);//清空集合
void FD_SET(int fd,fd_set *fdset);//将指定的描述符加入
void FD_CLR(int fd,fd_set *fdset);//删除指定的描述符
void FD_ISSET(int fd,fd_set *fdset);//检查是否可以读写
最后一个timeout,内核等待指定的描述字中就绪的时间长度
最后就是返回值是int型:失败-1;超时0;成功>0.
调用过程
可以看到,需要两次拷贝,耗时,且只能轮询fd,而且有描述符大小限制。
B.poll
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
第一个参数是个结构体:
struct pollfd {
int fd;/* 文件描述符 */
short events;/* 等待的事件 */
short revents;/* 实际发生了的事件 */
} ;
这个结构体指定了一个被监听的文件描述符,也可以传递多个结构体,让poll监视多个文件描述符。events是用户设置的事件掩码,revents是事件结果的掩码。events中的请求事件都可能在此返回。
第二个是描述符个数,第三个是时间。
一些合法的事件描述就不罗列了,主要有读写,区分了普通和优先。
返回值也同select一样。
和select差不多,区别在于描述符个数可以突破select的限制(链表)。
调用过程同select,但没有连接限制。
C.epoll
此函数需要三个接口,听着就复杂,但是越复杂越厉害:
int epoll_creat(int size);
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
int epoll_wait(int epfd,struct epoll_event *event,int maxevents,int timeout);
我们挨个说:
1 epoll_creat
创建一个epoll句柄,size是要监听的数目大小,注意为最大监听+1。epoll句柄就会占用一个fd,所以之后要close掉,不然fd会耗尽。
2 epoll_ctl
这是事件注册函数,select是监听时告诉内核监听什么事件,这个是你要监听啥,先在我这注册。
epfd是第一个函数的返回值。
op是操作,有三个宏定义表示:EPOLL_CTL_ADD//注册新fd到epfd;EPOLL_CTL_MOD//修改已经注册的fd;EPOLL_CTL_DEL//从epfd删除一个fd。
第三个fd是要监听的fd
最后是一个结构体:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
告诉内核监听什么事。具体events不罗列了。
3 epoll_wait
等待事件发生,maxevents是告诉内核事件有多大,自然不能大于creat里的size。
函数返回值是需要处理的事件数目,0表示超时。
这个不同的是有两个工作模式:
LT(水平触发模式,也是默认模式):当wait函数检测到事件发生并通知到应用程序时,应用程序可以不立即处理该事件。下次调用wait函数时,会再次通知此事件。
ET(边沿触发模式):当wait函数检测到事件发生并通知到应用程序,应用程序必须立即处理该事件。如果不处理,下次wait不会通知,也就是此事件会丢失。此模式减少了事件被重复触发的次数,效率比LT高。但必须使用非阻塞接口,因为是立即处理,所以如果阻塞立即处理该事时,其余文件描述符就会饿死。
调用过程
优点:没有最大并发连接的限制,只有活跃fd才能回调。内存拷贝则是利用mmap()文件映射内存区的方式加速与内核空间的信息传递,二者共享一块内存。
用户发起read操作后,就可以去干其他事。内核在接收异步read后,进行数据准备和数据拷贝到用户区,此时发出signal通知用户read完成。用的不多。
其实IO理解起来没有那么复杂,只是IO复用理解起来比较吃力点。所以还是以例子来进行描述。
select:我要请女神吃饭,所以要知道女神回来了没,所以我告诉楼管帮我看一下。楼管不认识女神,所以只能在有一个回来时就问一下,而且记性不好,只能记1024个。
poll:记性好,每个都会问,没有人数限制
epoll:先注册,在住的时候就认识了所有人,所以不用每个个问,能直接认出你女神。
总而言之,不需要你在那里等着了,你可以去干其他事,女神回来了楼管会通知你,你直接一起去吃饭就好了。
IO复用是处理多并发连接的升级办法,不用多线程来处理,当连接数目变多,线程也就变多,切换和维持开销都增大。IO复用只需要一个线程就可以完成。