概述
本文转载自:https://blog.csdn.net/qq_36359022/article/details/81355897
epoll接口是为解决Linux内核处理大量文件描述符而提出的方案。该接口属于Linux下多路I/O复用接口中select/poll的增强。其经常应用于Linux下高并发服务型程序,特别是在大量并发连接中只有少部分连接处于活跃下的情况 (通常是这种情况),在该情况下能显著的提高程序的CPU利用率。
epoll采用的是事件驱动,并且设计的十分高效。在用户空间获取事件时,不需要去遍历被监听描述符集合中所有的文件描述符,而是遍历那些被内核I/O事件异步唤醒之后加入到就绪队列并返回到用户空间的描述符集合。
epoll提供了两种触发模式,水平触发(LT)和边沿触发(ET)。当然,涉及到I/O操作也必然会有阻塞和非阻塞两种方案。目前效率相对较高的是 epoll+ET+非阻塞I/O 模型,在具体情况下应该合理选用当前情形中最优的搭配方案。
接下来的讲解顺序为:
(1) epoll接口的一般使用
(2) epoll接口 + 非阻塞
(3) epoll接口 + 非阻塞 + 边沿触发
(4) epoll反应堆模型 (重点,Libevent库的核心思想)
一、epoll接口的基本思想概述
epoll的设计:
(1)epoll在Linux内核中构建了一个文件系统,该文件系统采用红黑树来构建,红黑树在增加和删除上面的效率极高,因此是epoll高效的原因之一。有兴趣可以百度红黑树了解,但在这里你只需知道其算法效率超高即可。
(2)epoll红黑树上采用事件异步唤醒,内核监听I/O,事件发生后内核搜索红黑树并将对应节点数据放入异步唤醒的事件队列中。
(3)epoll的数据从用户空间到内核空间采用mmap存储I/O映射来加速。该方法是目前Linux进程间通信中传递最快,消耗最小,传递数据过程不涉及系统调用的方法。
epoll接口相对于传统的select/poll而言,有以下优点:
(1)支持单个进程打开大数量的文件描述符。受进程最大打开的文件描述符数量限制,而不是受自身实现限制。而select单个进程能够打开的文件描述符的数量存在最大限制,这个限制是select自身实现的限制。通常是1024。poll采用链表,也是远超select的。
(2)Linux的I/O效率不会随着文件描述符数量的增加而线性下降。较之于select/poll,当处于一个高并发时(例如10万,100万)。在如此庞大的socket集合中,任一时间里其实只有部分的socket是“活跃”的。select/poll的处理方式是,对用如此庞大的集合进行线性扫描并对有事件发生的socket进行处理,这将极大的浪费CPU资源。因此epoll的改进是,由于I/O事件发生,内核将活跃的socket放入队列并交给mmap加速到用户空间,程序拿到的集合是处于活跃的socket集合,而不是所有socket集合。
(3)使用mmap加速内核与用户空间的消息传递。select/poll采用的方式是,将所有要监听的文件描述符集合拷贝到内核空间(用户态到内核态切换)。接着内核对集合进行轮询检测,当有事件发生时,内核从中集合并将集合复制到用户空间。 再看看epoll怎么做的,内核与程序共用一块内存,请看epoll总体描述01这幅图,用户与mmap加速区进行数据交互不涉及权限的切换(用户态到内核态,内核态到用户态)。内核对于处于非内核空间的内存有权限进行读取。
更多关于mmap可以看看我的另一篇博客了进行了解。
https://blog.csdn.net/qq_36359022/article/details/79992287
接下来我们结合epoll总体描述01与上述的内容,将图示进行升级为epoll总体描述02。
让我们来看看图的红黑树文件系统。以epfd为根,挂载了一个监听描述符和5个与客户端建立连接的cfd。fd的增删按照红黑树的操作方式,每一个文件描述符都有一个对应的结构,该结构为
/*
* -[ epoll结构体描述01 ]-
*/
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
在epoll总体描述02中,每个fd上关联的即是结构体epoll_event。它由需要监听的事件类型和一个联合体构成。一般的epoll接口将传递自身fd到联合体。
因此,使用epoll接口的一般操作流程为:
(1)使用epoll_create()创建一个epoll对象,该对象与epfd关联,后续操作使用epfd来使用这个epoll对象,这个epoll对象才是红黑树,epfd作为描述符只是能关联而已。
(2)调用epoll_ctl()向epoll对象中进行增加、删除等操作。
(3)调用epoll_wait()可以阻塞(或非阻塞或定时) 返回待处理的事件集合。
(3)处理事件。
/*
* -[ 一般epoll接口使用描述01 ]-
*/
int main(void)
{
/*
* 此处省略网络编程常用初始化方式(从申请到最后listen)
* 并且部分的错误处理省略,我会在后面放上所有的源码,这里只放重要步骤
* 部分初始化也没写
*/
// [1] 创建一个epoll对象
ep_fd = epoll_create(OPEN_MAX); /* 创建epoll模型,ep_fd指向红黑树根节点 */
listen_ep_event.events = EPOLLIN; /* 指定监听读事件 注意:默认为水平触发LT */
listen_ep_event.data.fd = listen_fd; /* 注意:一般的epoll在这里放fd */
// [2] 将listen_fd和对应的结构体设置到树上
epoll_ctl(ep_fd, EPOLL_CTL_ADD, listen_fd, &listen_ep_event);
while(1) {
// [3] 为server阻塞(默认)监听事件,ep_event是数组,装满足条件后的所有事件结构体
n_ready = epoll_wait(ep_fd, ep_event, OPEN_MAX, -1);
for(i=0; i<n_ready; i++) {
temp_fd = ep_event[i].data.fd;
if(ep_event[i].events & EPOLLIN){
if(temp_fd == listen_fd) { //说明有新连接到来
connect_fd = accept(listen_fd, (struct sockaddr *)&client_socket_addr, &client_socket_len);
// 给即将上树的结构体初始化
temp_ep_event.events = EPOLLIN;
temp_ep_event.data.fd = connect_fd;
// 上树
epoll_ctl(ep_fd, EPOLL_CTL_ADD, connect_fd, &temp_ep_event);
}
else { //cfd有数据到来
n_data = read(temp_fd , buf, sizeof(buf));
if(n_data == 0) { //客户端关闭
epoll_ctl(ep_fd, EPOLL_CTL_DEL, temp_fd, NULL) //下树
close(temp_fd);
}
else if(n_data < 0) {}
do {
//处理数据
}while( (n_data = read(temp_fd , buf, sizeof(buf))) >0 ) ;
}
}
else if(ep_event[i].events & EPOLLOUT){
//处理写事件
}
else if(ep_event[i].events & EPOLLERR) {
//处理异常事件
}
}
}
close(listen_fd);
close(ep_fd);
}
二、epoll水平触发(LT)、epoll边沿触发(ET)
(1) epoll水平触发, 此方式为默认情况。
当设置了水平触发以后,以可读事件为例,当有数据到来并且数据在缓冲区待读。即使我这一次没有读取完数据,只要缓冲区里还有数据就会触发第二次,直到缓冲区里没数据。
(2) epoll边沿触发,此方式需要在设置
listen_ep_event.events = EPOLLIN | EPOLLET; /*边沿触发 */
当设置了边沿触发以后,以可读事件为例,对“有数据到来”这件事为触发。
总结:
1.用高低电平举例子就是
水平触发:0为无数据,1为有数据。缓冲区有数据则一直为1,则一直触发。
边沿触发:0为无数据,1为有数据,只要在0变到1的上升沿才触发。
2.
缓冲区有数据可读,触发 ⇒ 水平触发
缓冲区有数据到来,触发 ⇒ 边沿触发
那么,为什么说边沿触发(ET) 的效率更高呢?
(1) 边沿触发只在数据到来的一刻才触发,很多时候服务器在接受大量数据时会先接受数据头部(水平触发在此触发第一次,边沿触发第一次)。
(2) 接着服务器通过解析头部决定要不要接这个数据。此时,如果不接受数据,水平触发需要手动清除,而边沿触发可以将清除工作交给一个定时的清除程序去做,自己立刻返回。
(3) 如果接受,两种方式都可以用while接收完整数据。
三、epoll + 非阻塞I/O
在第一大部分中的 一般epoll接口使用描述01代码中,我们使用的读取数据函数为read函数,在默认情况下此类函数是阻塞式的,在没有数据时会一直阻塞等待数据到来。
(1)数据到来100B,在epoll模式下调用read时,即使read()是阻塞式的也不会在这里等待,因为既然运行到read(),说明数据缓冲区已经有数据,因此这处无影响。
(2)在服务器开发中,一般不会直接用采用类似read()函数这一类系统调用(只有内核缓冲区),会使用封装好的一些库函数(有内核缓冲区+用户缓冲区)或者自己封装的函数。
例如:使用readn()函数,设置读取200B返回,假设数据到来100B,可读事件触发,而程序要使用readn()读200B,那么此时如果是阻塞式的,将在此处形成死锁
流程是:100B ⇒ 触发可读事件 ⇒ readn()调用 ⇒ readn()都不够200B,阻塞 ⇒ cfd又到来200B ⇒ 此时程序在readn()处暂停,没有机会调用epoll_wait() ⇒ 完成死锁
解决:
将该cfd在上树前设置为非阻塞式
/* 修改cfd为非阻塞读 */
flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(connect_fd, F_SETFL, flag);
四、epoll + 非阻塞I/O + 边沿触发
/*
* -[ epoll+非阻塞+边沿触发 使用描述01 ]-
*/
/* 其他情况不变,与*一般epoll接口使用描述01*描述一致
* 变化的仅有第二大部分和第三大部分的这两段代码即可
*/
/* ... ... */
listen_ep_event.events = EPOLLIN | EPOLLET; /*边沿触发 */
/* ... ... */
/* 修改cfd为非阻塞读 */
flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(connect_fd, F_SETFL, flag);
/* ... ... */
---------------------
作者:青城山小和尚
来源:CSDN
原文:https://blog.csdn.net/qq_36359022/article/details/81355897
版权声明:本文为博主原创文章,转载请附上博文链接!
最后
以上就是俏皮豌豆为你收集整理的Linux网络高并发服务器之epoll接口的全部内容,希望文章能够帮你解决Linux网络高并发服务器之epoll接口所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复