第一课-Epoll框架
- 为什么要用Epoll?
(1)阻塞型IO
阻塞是指没有获得资源则挂起进程,直到获得资源为止。被挂起的进程进入休眠状态,被调度器的运行队列移走,直到等待条件被满足。
非阻塞是不能进行设备操作时不挂起,或放弃,或反复查询,直到可以进行操作为止。
驱动程序常需要这种能力:当应用程序进行read(),write()等系统调用时,若设备的资源不能获取,而用户又希望以阻塞的方式访问设备,驱动程序应该在设备驱动程序的xxx_read(), xxx_write()等操作中将进程阻直到资源可以获取,以后,应用程序read(),write()等调用返回,整个过程仍然进行了正确的设备访问,用户并没有感知到;若用户以非阻塞的方式访问设备文件,则当设备资源不可获取时,设备驱动的xxx_read(),xxx_write()等操作应立即返回,read(),write()等系统调用也随即被访问。
阻塞不是低效率,如果设备驱动不阻塞, 用户想获取设备资源只能不断查询,消耗CPU资源,阻塞访问时,不能获取资源的进程将进入休眠,将CPU资源让给其他资源。阻塞的进程会进入休眠状态,因此,必须确保有一个地方能唤醒休眠的进程。唤醒进程的地方最大可能发生在中断里面,因为硬件资源获得的同时往往伴随着一个中断。
(2)多路复用
多路复用是指以同一传输媒质(线路)承载多路信号进行通信的方式。各路信号在送往传输媒质以前,需按一定的规则进行调制,以利于各路已调信号在媒质中传输,并不致混淆,从而在传到对方时使信号具有足够能量,且可用反调制的方法加以区分、恢复成原信号。多路复用常用的方法有频分多路复用和时分多路复用,码分多路复用的应用也在不断扩大。
频分多路复用FDM (Frequency Division Multiplexing)和时分多路复用TDM (Time Division Multiplexing)是两种最常用的多路复用技术(Multiplexing)。
目前我们使用的函数,有那个可以实现阻塞型IO和多路复用呢?当然即使select函数。
(3)select函数
select()的机制中提供一fd_set的数据结构,实际上是一long类型的数组, 每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他 文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成, 当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执 行了select()的进程哪一Socket或文件可读或可写。主要用于Socket通信当中!
我们看一下这个函数的一般使用过程:
需要头文件#include <sys/select.h>
while(1)
{
FD_ZERO(&set); //1. 初始化文件描述符集合
foreach(需要监控的文件)
{
fd大于maxfd,则maxfd-fd
FD_SET(fd,&set) //2.将文件的描述符一个一个的加到文件中去
}
res=select(maxfd+1,&set,0,0,0); /*3. 调用select开始监控,要是没有文件满足要求,就会等待在这个地方,
等到要么超时,要么就等待在这个地方*/
if(FD_ISSET(listen_fd,&set))
{
newfd=accept(listen_fd);
array[nsock]=newfd;
if(--res<=0) continue;
}
foreach(需要监控的文件) /*4. 遍历文件,看看是哪个的变化导致的这种退出,很占用资源*/
{
if(FD_ISSET(fd,&tyle="COLOR: #ff0000">set))
执行读等相关的操作
如果错误或者关闭,则要删除fd,将array中相应位置和最后而定一个元素相互调换就好,nsock减一
if(--res<=0) continue;
}
}
介于select函数在使用的时候,由于遍历性使得运行的时间很长,很浪费资源,所以我们引出了下面的epoll函数。
(4)Epoll分析
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
无序遍历,没有上线,是linux系统中最优秀的多路复用机制。
- 怎么使用Epoll?
Epoll支持管道,FIFO,套接字,POSIX消息队列,终端,设备等等,但就是不支持普通文件。Epoll的使用分为3个环节。
epoll_creat/epoll_creat1(创建epoll监听池)
epoll_ctl(添加要监听的事件)
epoll_wait(等待事件的发生)
(1)epoll_creat/epoll_creat1
创建epoll监听池
synopsis:(梗要)
#include <sys/epoll.h>
int epoll_creat(int size); //老的版本中size表示监听池的大小,现在没用了
int epoll_creat1(int flags); /*flags表示创建标志,一般的情况下,我么让它等于0,当它等于0的时候,这个函数和epoll_creat()函数的作用是一模一样的 */
description:
epoll_creat() craets an epoll “instance”, requesting the kernel to allocate an event backing store dimensioned for size descriptors. The size is not the maxmum size of the backing store but just a hint to the kernel about how to dimension internal structures.
epoll_creat() returns a file descriptor referring to the new epoll instance. This file descriptor is used for all the subsequent calls to the epoll interface.
返回值:
成功,无;失败,-1。
(2)epoll_ctl
添加要监听的事件, select是以文件作为监听对象的,但是epoll_ctl是以事件为监听对象的。A文件(可读)和A文件(可写)算是两个文件
synopsis:(梗要)
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
description:
This system call performs control operations on the epoll instance referred toby the file descriptor epfd. It requests that the operation op be performed for the target file dercriptor, fd.
epfd我们要监听的监听池的fd;
op表示我们对监听池进行的操作:
EPOLL_CTL_ADD:加入事件
EPOLL_CTL_MOD:更改事件
EPOLL_CTL_DEL:删除事件
fd表示我们要操作的事件是哪一个文件
*event表示我们操作的类型:
EPOLLIN:可读
EPOLLOUT:可写
返回值:成功0,失败-1.
(3)epoll_wait
等待事件的发生
synopsis:(梗要)
#include <sys/epoll.h>
int epoll_wait(int epfd struct epoll_event *event, int maxevent, int timeout);
description:
epfd:等待的事件
*event:当事件被监听到的时候,就会被放到这个数组里面
maxevent:最多的时间数
timeout:等待的最长时间
返回值:
成功,表示有多少个事件发生;失败,-1。
- 程序-监听两个FIFO
创建文件ep.c,ew1.c和ew2.c。文件ep.c是我们创建的epoll监控文件,ew1.c和ew2.c文件分别是我们对创建的两个FIFO文件的写入文件。程序如下:
ep.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/epoll.h>
int main()
{
int fd1, fd2;
int efd;
int i;
char c;
int n;
struct epoll_event event;
struct epoll_event *events;
//创建FIO
mkfifo("/tmp/fifo1",0666);
mkfifo("/tmp/fifo2",0666);
fd1=open("/tmp/fifo1",O_RDONLY);
fd2=open("/tmp/fifo2",O_RDONLY);
//创建监听池
efd=epoll_create1(0);
//构造监听事件,加入监听池
event.events = EPOLLIN|EPOLLET; /*EPOLLET表示边沿触发*/
event.data.fd = fd1;
epoll_ctl(efd,EPOLL_CTL_ADD,fd1,&event);
event.events = EPOLLIN;
event.data.fd = fd2;
epoll_ctl(efd,EPOLL_CTL_ADD,fd2,&event);
//开始监听
events = calloc(100,sizeof(event)); //给要保存的数组分配空间,可以保存100个事件
n = epoll_wait(efd,events,100,-1);
for(i=0;i<n;i++)
{
if(events[i].events & EPOLLIN) //如果数据类型是EPOLLIN
{
read(events[i].data.fd,&c,1);
//假设我们读的是个字符,长度当然就是1,要读的文件在events[i].data.fd中
printf("file %d can be read ",events[i].data.fd);
}
if(events[i].events & EPOLLOUT) //如果数据类型是EPOLLOUT
{
//
}
if(events[i].events & EPOLLERR) //如果数据类型是EPOLLERR,出错的
{
//
}
}
free(events); //释放我们使用的空间,必须得有
close(fd1);
close(fd2); //关闭打开的文件
}
ew1.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
int fd;
char c1 = 'c';
fd = open("/tmp/fifo1", O_WRONLY); //用只写的方式打开
write(fd,&c1,1);
close(fd);
return 0;
}
ew2.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
int fd;
char c1 = 'c';
fd = open("/tmp/fifo2", O_WRONLY); //用只写的方式打开
write(fd,&c1,1);
close(fd);
return 0;
}
分别用gcc编译成ep,ew1和ew2三个.o文件。在一个终端中运行./ep会是程序卡住,使用ctrl+shift+t生成另一个相同的中断,运行./ew1和./ew2后,第一个中断可以继续运行,结果为:
file 3 can be read
file 4 can be read