epoll是Linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超select和poll。目前流行的高性能web服务器Nginx正式依赖于epoll提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程+阻塞I/O方式可能性能更好。
下面一张图总结select,poll,epoll的区别:
他们属于不同时期产物,新技术当然是更加好用。
select出现是1984年在BSD里面实现的。
14年之后也就是1997年才实现了poll,其实拖那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求。
2002, 大神 Davide Libenzi 实现了epoll。
epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式,相对于select来说,epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
#include
int epoll_create(int size);
函数功能: 创建epoll专用文件描述符的缓冲区。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数功能: 添加、删除、修改 监听文件描述符。
函数参数:
int epfd epoll专用的文件描述符
int op 操作命令。EPOLL_CTL_ADD EPOLL_CTL_MOD EPOLL_CTL_DEL
int fd 要操作文件描述符
struct epoll_event *event 存放监听文件描述符信息的结构体
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
函数功能: 等待事件发生。
函数参数:
int epfd epoll专用的文件描述符
struct epoll_event *events : 存放产生事件的文件描述结构体。
int maxevents :最大监听的数量.
int timeout :等待事件ms单位. <0 >0 ==0
返回值: 产生事件的数量。
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 EPOLLIN 输入事件 */
epoll_data_t data; /* User data variable */
};
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
LT和ET原本应该是用于脉冲信号的,可能用它来解释更加形象。Level和Edge指的就是触发点,Level为只要处于水平,那么就一直触发,而Edge则为上升沿和下降沿的时候触发。比如:0->1 就是Edge,1->1 就是Level。
ET模式很大程度上减少了epoll事件的触发次数,因此效率比LT模式下高。
epoll 的使用非常简单,只有下面 3 个系统调用。
epoll_createepollctlepollwait
就这?是的,就这么简单。
Linux 下,epoll 一直被吹爆,作为高并发 IO 实现的秘密武器。其中原理其实非常朴实:epoll 的实现几乎没有做任何无效功。 我们从使用的角度切入来一步步分析下。
首先,epoll 的第一步是创建一个池子。这个使用 epoll_create 来做:
原型:
int epoll_create(int size);
示例:
epollfd = epoll_create(1024);if (epollfd == -1) { perror("epoll_create"); exit(EXIT_FAILURE);}
这个池子对我们来说是黑盒,这个黑盒是用来装 fd 的,我们暂不纠结其中细节。我们拿到了一个 epollfd ,这个 epollfd 就能唯一代表这个 epoll 池。注意,这里又有一个细节:用户可以创建多个 epoll 池。
然后,我们就要往这个 epoll 池里放 fd 了,这就要用到 epoll_ctl 了
原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
示例:
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, 11, &ev) == -1) { perror("epoll_ctl: listen_sock"); exit(EXIT_FAILURE);}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//消息结构体
struct MSG_DATA
{
char type; //消息类型. 0表示有聊天的消息数据 1表示好友上线 2表示好友下线
char name[50]; //好友名称
int number; //在线人数的数量
unsigned char buff[100]; //发送的聊天数据消息
};
struct MSG_DATA msg_data;
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int epollfd;
int nfds;
//文件接收端
int main(int argc,char **argv)
{
if(argc!=4)
{
printf("./app <端口号> <名称>
");
return 0;
}
int sockfd;
//忽略 SIGPIPE 信号--方式服务器向无效的套接字写数据导致进程退出
signal(SIGPIPE,SIG_IGN);
/*1. 创建socket套接字*/
sockfd=socket(AF_INET,SOCK_STREAM,0);
/*2. 连接服务器*/
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(atoi(argv[2])); // 端口号0~65535
addr.sin_addr.s_addr=inet_addr(argv[1]); //IP地址
if(connect(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr_in))!=0)
{
printf("客户端:服务器连接失败.
");
return 0;
}
/*3. 发送消息表示上线*/
msg_data.type=1;
strcpy(msg_data.name,argv[3]);
write(sockfd,&msg_data,sizeof(struct MSG_DATA));
int cnt;
int i;
//创建专用文件描述符
epollfd = epoll_create(10);
//添加要监听的文件描述符
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev);
ev.events = EPOLLIN;
ev.data.fd = 0; //标准输入文件描述符
epoll_ctl(epollfd, EPOLL_CTL_ADD, 0, &ev);
while(1)
{
//监听事件
nfds=epoll_wait(epollfd,events,MAX_EVENTS,-1);
if(nfds)
{
for(i=0;i0)
{
if(msg_data.type==0)
{
printf("%s:%s 在线人数:%d
",msg_data.name,msg_data.buff,msg_data.number);
}
else if(msg_data.type==1)
{
printf("%s 好友上线. 在线人数:%d
",msg_data.name,msg_data.number);
}
else if(msg_data.type==2)
{
printf("%s 好友下线. 在线人数:%d
",msg_data.name,msg_data.number);
}
}
}
else if(events[i].data.fd==0) //表示键盘上有数据输入
{
gets(msg_data.buff); //读取键盘上的消息
msg_data.type=0; //表示正常消息
strcpy(msg_data.name,argv[3]); //名称
write(sockfd,&msg_data,sizeof(struct MSG_DATA));
}
}
}
}
SERVER_ERROR:
close(sockfd);
return 0;
}
在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。更多framework进阶学习;可私信前往:发送“核心笔记”或“手册”,即可获取相关资料!
留言与评论(共有 0 条评论) “” |