IO模型-libevent打基础

在看libevent模型之前,先对IO模型,主要是IO复用进行一个归纳总结和理解。

1. 同步阻塞IO

用户进程向系统发起read操作时,要在内核中进行数据准备从内核到用户进程的拷贝(拷贝到用户进程用户进程才能使用)。此时,read操作才算完成,而后再进行处理。
我想请女神出去吃饭,于是给女神发微信我在楼下等你,期间什么也不干,就阻塞在楼下等待女神下来,然后去吃饭。

2. 同步非阻塞IO

socket默认都是阻塞的。非阻塞则要将socket设置为NONBLOCK。非阻塞IO不同的是,每次read操作都会返回状态。可能准备好了,可能没有。因此用户进程需要不断发起read请求,去查询数据准备好了没有。可以理解为一个while循环,没有准备好,我就一直请求。
我想请女神吃饭,发了信息女神没下来,于是我就继续发,还没下来,继续。(女神生气了,嫌你烦,一直不下来,你就一直发,太耗电了,耗流量还。反应到编程中就是太耗CPU了。)直到女神下来,然后去吃饭。

3. IO多路复用

上面的IO操作都是用户亲自去做的。IO复用的话,我们不亲自进行询问,我们委托一个函数帮我们去等。也就是委托专业的IO监测函数来干,比我们干的活多。
在这里插入图片描述
IO复用流程如上:

  1. 当进程调用select时,就陷入阻塞;
  2. 内核就开始监视所有select负责的socket,当socket的数据准备好后就立即返回;
  3. 准备好了,read再次调用,将数据从内核拷贝到用户进程(这就是开始说的,需要进行数据准备,然后进行数据拷贝)

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.
调用过程

  1. fd_set从用户空间拷贝到内核空间
  2. 注册回调函数
  3. 调用对应的poll方法
  4. 返回一个就绪与否的掩码,给fdset赋值
  5. select遍历fd检查有无就绪掩码,没有就重新遍历,有则唤醒等待进程
  6. fd_set从内核空间拷贝到用户空间

可以看到,需要两次拷贝,耗时,且只能轮询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高。但必须使用非阻塞接口,因为是立即处理,所以如果阻塞立即处理该事时,其余文件描述符就会饿死。
调用过程

  1. 调用epoll_wait函数时,系统会创建一个epoll对象,并有对应的结构体与之对应。
  2. 当fd就绪,就会触发回调函数。
  3. 回调函数会将ready状态的fd放入readylist,用户进程被唤醒,wait函数继续执行
  4. events和maxevents两个参数描述一个由用户分配的struct epoll event数组,调用返回时,内核将readylist复制到这个数组中,并将实际复制的个数作为返回值。

优点:没有最大并发连接的限制,只有活跃fd才能回调。内存拷贝则是利用mmap()文件映射内存区的方式加速与内核空间的信息传递,二者共享一块内存。

异步IO

用户发起read操作后,就可以去干其他事。内核在接收异步read后,进行数据准备和数据拷贝到用户区,此时发出signal通知用户read完成。用的不多。

其实IO理解起来没有那么复杂,只是IO复用理解起来比较吃力点。所以还是以例子来进行描述。
select:我要请女神吃饭,所以要知道女神回来了没,所以我告诉楼管帮我看一下。楼管不认识女神,所以只能在有一个回来时就问一下,而且记性不好,只能记1024个。
poll:记性好,每个都会问,没有人数限制
epoll:先注册,在住的时候就认识了所有人,所以不用每个个问,能直接认出你女神。
总而言之,不需要你在那里等着了,你可以去干其他事,女神回来了楼管会通知你,你直接一起去吃饭就好了。

IO复用是处理多并发连接的升级办法,不用多线程来处理,当连接数目变多,线程也就变多,切换和维持开销都增大。IO复用只需要一个线程就可以完成。

发布了22 篇原创文章 · 获赞 0 · 访问量 349

猜你喜欢

转载自blog.csdn.net/yanchenzhi/article/details/105144651
今日推荐