EPOLL简介:
Epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序存在并发中只有少量活跃的情况下的系统利用率,因为它会服用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列中的描述符集合。
一、Linux下EPOLL的基本API
int epoll_create(int size) ; 创建一个文件描述符句柄,创建一颗监听红黑树
int epoll_create(int size) ; //创建一个文件描述符句柄,创建一颗监听红黑树
/* 参数:size:创建的红黑树监听节点数量(仅供内核参考)
返回值:指向新创建的红黑树的根节点fd
失败:-1 errno
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 操作监听红黑树:操作内核时间表监控的文件描述符上的事件:注册、修改、删除
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); //操作监听红黑树
/* 参数:epfd: epoll_create函数的返回值。
op: 表示对该监听红黑树所做的操作
EPOLL_CTL_ADD 添加fd到监听红黑树
EPOLL_CTL_MOD 修改fd在监听红黑树上的监听事件
EPOLL_CTL_DEL 将一个fd从监听红黑树摘下(取消监听)
fd: 待监听的fd
event: 本质是struct epoll_event结构体指针(传出参数)
events:
EPOLLIN : 读事件;
EPOLLOUT: 写事件;
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET : 将 EPOLL设为边缘触发(Edge Triggered)模式(默认为水平触发),这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
EPOLLRDHUP: 表示对端断开连接
data: 联合体(共用体):
int fd; 对应监听事件的fd
void* ptr; 设置回调函数(内核自动调用)
struct evt{
int fd;
void (*func)(int a);
}*ptr
uint32_t u32;
uint64_t u64;
返回值:成功0,失败返回1 */
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); 阻塞监听:用于等待所监控文件描述符上有事件的产生,返回就绪的文件描述符个数
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 阻塞监听
/*
参数:epfd: epoll_create函数的返回值。
events: 传出参数【数组】,满足监听条件的fd结构体
maxevents:表示数组元素的总个数
timeout: >0 超时时间
-1 阻塞
0 不阻塞
返回值: >0 满足监听的总个数。可以用作循环上限
(不需要轮询1024,或者设置额外数组)
0 没有fd满足监听事件
-1 失败 errno
*/
二、EPOLL的边沿触发和水平触发
ET模式:
* 边沿模式
* 缓冲区剩余未读进的数据不会对epoll_wait触发返回,新的事件满足才会触发
* 必须一次性将数据读完,使用非阻塞I/O
* struct epoll_event event;
event.events=EPOLLIN | EPOLLET;
LT模式:
* 水平触发 ---- 默认采用模式
* 缓冲区剩余未读进的数据会对epoll_wait触发返回
EPOLLONESHOT:
-
一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket
-
我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件
三、select、poll与epoll的区别与选择
-
调用函数
-
select和poll都是一个函数,epoll是一组函数
-
-
文件描述符数量
-
select通过线性表描述文件描述符集合,文件描述符有上限,一般是1024,但可以修改源码,重新编译内核,不推荐
-
poll是链表描述,突破了文件描述符上限,最大可以打开文件的数目
-
epoll通过红黑树描述,最大可以打开文件的数目,可以通过命令ulimit -n number修改,仅对当前终端有效
-
-
将文件描述符从用户传给内核
-
select和poll通过将所有文件描述符拷贝到内核态,每次调用都需要拷贝
-
epoll通过epoll_create建立一棵红黑树,通过epoll_ctl将要监听的文件描述符注册到红黑树上
-
-
内核判断就绪的文件描述符
-
select和poll通过遍历文件描述符集合,判断哪个文件描述符上有事件发生
-
epoll_create时,内核除了帮我们在epoll文件系统里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。
-
epoll是根据每个fd上面的回调函数(中断函数)判断,只有发生了事件的socket才会主动的去调用 callback函数,其他空闲状态socket则不会,若是就绪事件,插入list
-
-
应用程序索引就绪文件描述符
-
select/poll只返回发生了事件的文件描述符的个数,若知道是哪个发生了事件,同样需要遍历
-
epoll返回的发生了事件的个数和结构体数组,结构体包含socket的信息,因此直接处理返回的数组即可
-
-
工作模式
-
select和poll都只能工作在相对低效的LT模式下
-
epoll则可以工作在ET高效模式,并且epoll还支持EPOLLONESHOT事件,该事件能进一步减少可读、可写和异常事件被触发的次数。
-
-
应用场景
-
当所有的fd都是活跃连接,使用epoll,需要建立文件系统,红黑书和链表对于此来说,效率反而不高,不如selece和poll
-
当监测的fd数目较小,且各个fd都比较活跃,建议使用select或者poll
-
当监测的fd数目非常大,成千上万,且单位时间只有其中的一部分fd处于就绪状态,这个时候使用epoll能够明显提升性能
-
四、EPOLL实现多路IO转接简单实例
功能:实现多路IO复用,客户端输入小写字符串,服务器返回大写字符串
server端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>
#define MAXLINE 80
#define SERV_PORT 6666
#define OPEN_MAX 1024
int main()
{
int i,j,maxi,listenfd,connfd,sockfd;
int nready,efd,res;
ssize_t n;
char buf[MAXLINE],str[INET_ADDRSTRLEN];
socklen_t clilen;
int client[OPEN_MAX];
struct sockaddr_in cliaddr,servaddr;
struct epoll_event tep,ep[OPEN_MAX]; //ep传出参数,满足监听事件的数组
listenfd = socket(AF_INET,SOCK_STREAM,0);
bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port=htons(SERV_PORT);
bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
listen(listenfd,20);
for(i=0;i<OPEN_MAX;i++)
{
client[i] = -1;
}
maxi =-1;
//创建红黑树句柄
efd=epoll_create(OPEN_MAX);
if(efd == -1){
perror("epoll_create error");
}
tep.events = EPOLLIN;
tep.data.fd=listenfd;
//将监听描述符添加到红黑树
res=epoll_ctl(efd,EPOLL_CTL_ADD,listenfd,&tep);
if(res == -1){
perror("epoll_ctl error");
}
while(1)
{
nready = epoll_wait(efd,ep,OPEN_MAX,-1);
if(nready == -1){
perror("epoll_wait error");
}
//遍历就绪队列
for(i=0;i<nready;i++)
{
if(!(ep[i].events & EPOLLIN)){
continue;
}
//listenfd满足读事件:有新的客户端连接
if(ep[i].data.fd == listenfd){
clilen=sizeof(cliaddr);
connfd=accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (j = 0; j < OPEN_MAX; j++) {
if (client[j] < 0) {
client[j] = connfd; /* save descriptor */
break;
}
}
if (j == OPEN_MAX){
perror("too many clients");
exit(1);
}
if (j > maxi)
maxi = j;
//将connfd事件添加到红黑树
tep.events = EPOLLIN;
tep.data.fd = connfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep);
if (res == -1){
perror("epoll_ctl");
exit(1);
}
}
//监听connfd,有客户端满足写事件
else{
sockfd = ep[i].data.fd;
n=read(sockfd,buf,MAXLINE);
if(n == 0){
for (j = 0; j <= maxi; j++) {
if (client[j] == sockfd) {
client[j] = -1;
break;
}
}
//将关闭的cfd,从监听客户端摘除
res=epoll_ctl(efd,EPOLL_CTL_DEL,sockfd,NULL);
if(res==-1){
perror("epoll_ctl error");
exit(1);
}
close(sockfd);
printf("client[%d] closed connection\n", j);
}
else{
//字符串转大写并写回sockfd
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
write(sockfd, buf, n);
}
}
}
}
close(listenfd);
close(efd);
return 0;
}
client端:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 80
#define SERV_PORT 6666
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
while (fgets(buf, MAXLINE, stdin) != NULL) {
write(sockfd, buf, strlen(buf));
n = read(sockfd, buf, MAXLINE);
if (n == 0)
printf("the other side has been closed.\n");
else
write(STDOUT_FILENO, buf, n);
}
close(sockfd);
return 0;
}