Epoll多路I/O复用技术
通常学习一个新的linux技术,我们应该看看man手册对其定义。
NAME
epoll - I/O event notification facility
SYNOPSIS
#include <sys/epoll.h>
DESCRIPTION
The epoll API performs a similar task to poll(2): monitoring multiple file descriptors to see if I/O is possible on any of them.
The epoll API can be used either as an edge-triggered or a level-triggered interface and scales well to large numbers of
watched file descriptors.
那么从man手册这段文字我们可以看出,epoll它是由linux另一套的并发处理方案poll演变过来的,它与poll相类似:能够监控多个文件描述符的I/O变化。在Linux中,一切皆文件(有部分不是)所以,任何一个连接,也有一个文件描述符(一般为int类型)来存放
重点:epoll比poll的优点:支持水平触发(level-triggered)和边沿触发(edge-triggered)两种方案
Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据。
Level Triggered (LT) 水平触发只要有数据都会触发。
epoll所需的API函数:
epoll_create(2) creates an epoll instance and returns a file descriptor referring to that instance. (The more recent epoll_create1(2) extends the functionality of epoll_create(2).)
Interest in particular file descriptors is then registered via epoll_ctl(2). The set of file descriptors currently registered on an epoll instance is sometimes called an epoll set.
epoll_wait(2) waits for I/O events, blocking the calling thread if no events are currently available.
1、创建epoll文件描述符
创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
int epoll_create(int size); //size:监听数目
2、管理epoll中的文件描述符集合,增加、修改、删除
epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数是epoll_create()的返回值。
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd。
第四个参数是告诉内核需要监听什么事
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;
3、收集在epoll监控的事件中已经发送的事件(默认阻塞等待)
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
/*
events:用来从内核得到事件的集合,
maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
timeout:是超时时间
-1:阻塞
0:立即返回,非阻塞
>0:指定微秒
返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
*/
epoll工作原理
1、epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
2、另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
epoll 服务端例子
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main()
{
int sock_server = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10099);
addr.sin_addr.s_addr = 0;
int ret = bind(sock_server,(struct sockaddr*)&addr,sizeof(addr));
listen(sock_server,5);
int epollfd = epoll_create(2);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sock_server;
//吧sock_server加入eopll集合中
epoll_ctl(epollfd,EPOLL_CTL_ADD,sock_server,&ev);
while(1)
{
struct epoll_event outev[8];
int ret = epoll_wait(epollfd,outev,8,1000);
if (ret < 0)
{
if (errno == EINTR)//若被信号打断,则重新循环
continue;
break;
}
if (ret > 0)//有被唤醒的文件描述符
{
for(int i = 0 ; i<ret; i++)
{
int fd = outev[i].data.fd;
if (fd == sock_server)
{
//若为socket的文件描述符,则用accept进行三次握手,建立连接
int newfd = accept(fd,NULL,NULL);
//EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
ev.events = EPOLLIN;
ev.data.fd = newfd;
//把新的fd加到epollfd中,继续等待下一次唤醒
epoll_ctl(epollfd,EPOLL_CTL_ADD,newfd,&ev);
}
else
{
//若不是socketfd,则为已经建立的连接,可以直接读取数据
char buf[1024];
int readlen = read(fd,buf,sizeof(buf));
if (readlen<=0)
{
//read<=0,证明已经没有数据,或者出错,关闭fd
close(fd);
}
else
{
printf("read data is :%s\n",buf);
}
}
}
}
}
return 0;
}
测试epoll的客户端
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main()
{
int fd = socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10099);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
int ret = connect(fd , (struct sockaddr*)&addr, sizeof(addr));
write(fd,"hello server",sizeof("hello server"));
char buf[1024];
read(fd,buf,sizeof(buf));
printf("server:%s\n",buf);
close(fd);
return 0;
}
测试结果我就暂时不贴图了,希望这次博客能给大家带来一点收获!