epoll简介和使用
2013-10-07 23:10
585 查看
epoll是Linux内核为处理大批量句柄而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著减少程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
说起epoll,就不得不提起select。对比起select,epoll主要有下面3个优点:
1、[句柄数] select最大句柄数受限,默认为1024(FD_SETSIZE值,可通过内核修改);而epoll最大句柄数为进程打开文件的句柄数,只受资源限制
2、[检查IO事件方法] select采用轮询操作去查询监听的每个句柄是否为IO事件;而epoll则在每个监听句柄上有个回调函数(callback),当句柄的IO事件发生时,会自动调用回调函数通知epoll,故epoll只需维护一个“活跃句柄”队列。这也是epoll适合大量并发连接中只有少量活跃的原因
3、[内存拷贝] 无论是select还是epoll,都需要把内核空间的句柄消息通知给进程空间。select直接将内核空间的消息在内存上拷贝一份到用户空间;而epoll使用mmap一块内核和用户空间共用的内存来减少内存拷贝。
// =============================================================================================
先直接上代码,用一个最基础的例子来直观了解epoll。下面是使用epoll写的一个简单TCP服务器(epollSvr.cpp):
这里简单分析下:
1、当客户端调用sock.connect( ),触发epoll事件,服务端if (sEvents[i].data.fd == iSvrFd){ }成立,建立新连接,输出“New Con From 127.0.0.1:63937”
2、当客户端调用sock.send( ),触发epoll事件,服务端if (sEvents[i].events & EPOLLIN){ }成立,读取客户端发送数据,输出“[EPOLLIN] Recv Info:Hello Libevent, I`m Cli A”
3、服务端在第2步时注册了EPOLLOUT,当写缓冲没有满时,会自动触发epoll事件,服务端if (sEvents[i].events & EPOLLOUT) { },向客户端发送数据,输出“[EPOLLOUT] Send Suc”
4、当客户端调用sock.close( ),触发epoll事件,服务端if (sEvents[i].events & EPOLLIN){ }成立,读取不到客户端发送数据,证明为客户端关闭连接了,所以注销句柄,输出“[EPOLLIN] Con Closed”
// =============================================================================================
* epoll的2种触发模式
1、水平模式:Level Triggered(LT),默认工作方式。支持阻塞和非阻塞Socket。这种模式下,epoll会告诉你某个句柄有IO事件,如果你没有处理(epoll_wait()出来之后没有accept或者read等),其会一直通知下去
2、边缘触发:Edge Triggered(ET),高速工作方式。只支持非阻塞Socket(为什么?)。这种模式下,epoll只会将句柄的IO事件通知你一次,如果你没有处理,这个IO事件将会丢失
* epoll相关的数据结构
EPOLLIN :文件描述符可以读(包括客户端connect,send/sendto,close);
EPOLLOUT:文件描述符可以写;
EPOLLPRI:文件描述符有紧急的数据可读;
EPOLLERR:文件描述符发生错误;
EPOLLHUP:文件描述符被挂断;
EPOLLET: 将epoll设为ET触发模式,因为默认为LT触发。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
比如你要以ET触发模式监听某句柄的读事件,则设events=EPOLLIN|EPOLLET
* epoll的3个相关函数
epoll的API还是很简单的,只有3个函数,epoll_create( ) / epoll_ctl( ) / epoll_wait( )
1、epoll_create(int maxfds):创建一个epoll句柄,maxfds为这个epoll支持的最大句柄数
2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event):事件控制函数,用来注册/修改/删除事件
第一个参epfd为调用epoll_create( )创建的epoll句柄
第二个参op为事件类型,EPOLL_CTL_ADD为注册,EPOLL_CTL_MOD为修改,EPOLL_CTL_DEL为删除
第三个参fd为要监听的句柄
第四个参event为这个监听事件的具体信息,如要监听这个句柄的读事件还是写
尽量少地调用epoll_ctl,防止其所引来的开销抵消其带来的好处。有的时候,应用中可能存在大量的短连接(比如说Web服务器),epoll_ctl将被频繁地调用,可能成为这个系统的瓶颈
3、int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout):调用一次,epoll就查询当前的网络接口,检测在epoll注册的所有句柄有没有IO事件发生
第一个参epfd为epoll句柄
第二个参events作为一个回参,存放epoll检测到有IO事件发生的句柄信息
第三个参maxevents为第二个参events的长度
第四个参timeout为调用epoll_wait( )的超时时间,单位为ms(0表示立即返回,即非阻塞;-1表示阻塞)
* EPOLLOUT事件什么时候被触发?
这个问题是我一开始学习epoll时有些不解的,因为客户端给服务端发送数据,服务端的接收缓冲区有数据可读而触发EPOLLIN事件。但是写事件是服务端的一个主动行为,服务端怎么主动自己触发自己?
后来发现这个问题其实很简单,就是只要“缓冲区不满”,服务端能写数据,就会一直触发EPOLLOUT。
* 怎么判断客户端连接结束并将对应句柄从epoll中删去?
1、当客户端断开连接,会触发EPOLLIN事件。所以如果在EPOLLIN逻辑里,read返回0(读不到数据,且没有出错),可判断为客户端连接结束。
2、只要将客户端对应的句柄close( )掉,epoll自动会将其从监听句柄中删去。
说起epoll,就不得不提起select。对比起select,epoll主要有下面3个优点:
1、[句柄数] select最大句柄数受限,默认为1024(FD_SETSIZE值,可通过内核修改);而epoll最大句柄数为进程打开文件的句柄数,只受资源限制
2、[检查IO事件方法] select采用轮询操作去查询监听的每个句柄是否为IO事件;而epoll则在每个监听句柄上有个回调函数(callback),当句柄的IO事件发生时,会自动调用回调函数通知epoll,故epoll只需维护一个“活跃句柄”队列。这也是epoll适合大量并发连接中只有少量活跃的原因
3、[内存拷贝] 无论是select还是epoll,都需要把内核空间的句柄消息通知给进程空间。select直接将内核空间的消息在内存上拷贝一份到用户空间;而epoll使用mmap一块内核和用户空间共用的内存来减少内存拷贝。
// =============================================================================================
先直接上代码,用一个最基础的例子来直观了解epoll。下面是使用epoll写的一个简单TCP服务器(epollSvr.cpp):
#include <iostream> #include <stdio.h> #include <string.h> #include <errno.h> #include <fcntl.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> using namespace std; // epoll的头文件 #include <sys/epoll.h> const int mMaxPending = 10; const int mMaxBufSize = 1500; // 将Socket设置为非阻塞模式(epoll的ET模式只支持非阻塞Socket) int setNonblocking(int fd) { if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0)|O_NONBLOCK) == -1) { return -1; } return 0; } int main() { int iSvrFd, iCliFd; struct sockaddr_in sSvrAddr, sCliAddr; memset(&sSvrAddr, 0, sizeof(sSvrAddr)); sSvrAddr.sin_family = AF_INET; sSvrAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); sSvrAddr.sin_port = htons(8888); // 创建tcpSocket(iSvrFd),监听本机8888端口 iSvrFd = socket(AF_INET, SOCK_STREAM, 0); setNonblocking(iSvrFd); bind(iSvrFd, (struct sockaddr*)&sSvrAddr, sizeof(sSvrAddr)); listen(iSvrFd, mMaxPending); int i, iEpollFd, iCnt; struct epoll_event sEvent, sEvents[10]; // 1.epoll_create(int maxfds):创建一个epoll句柄,maxfds为这个epoll支持的最大句柄数 iEpollFd = epoll_create(256); // 将上面的tcpSocket构造为epoll的Event sEvent.data.fd = iSvrFd; sEvent.events = EPOLLIN|EPOLLET; // 2.epoll_ctl(int epfd, int op, int fd, struct epoll_event *event):epoll的事件控制函数,用来注册/修改/删除事件。 // 这里将刚构建的Event注册进来 epoll_ctl(iEpollFd, EPOLL_CTL_ADD, iSvrFd, &sEvent); while(1) { // 3.int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) // 查询网络接口,检测在epoll注册的所有句柄有没有IO事件发生 iCnt = epoll_wait(iEpollFd, sEvents, 10, 1000); for(i = 0; i < iCnt; i ++) { if (sEvents[i].data.fd == iSvrFd) { // A.tcpSocket有事件发生,证明有新连接请求 socklen_t iSinSize = sizeof(sCliAddr); iCliFd = accept(iSvrFd, (struct sockaddr*)&sCliAddr, &iSinSize); cout << "New Con From " << inet_ntoa(sCliAddr.sin_addr) << ":" << sCliAddr.sin_port << endl; setNonblocking(iCliFd); // 将新的客户端连接注册到epoll sEvent.data.fd = iCliFd; sEvent.events = EPOLLIN|EPOLLET; epoll_ctl(iEpollFd, EPOLL_CTL_ADD, iCliFd, &sEvent); } else if (sEvents[i].events & EPOLLIN) { // B.客户端连接读事件 // 这里分2种情况,客户端发送过来数据包,或者客户端关闭连接,都会触发EPOLLIN cout << "[EPOLLIN]" << endl; int iLen; char buf[mMaxBufSize+1]; // 调用recv接收客户端发送过来的数据包 if ((iLen = recv(iCliFd, buf, mMaxBufSize, 0)) < 0) { perror("Recv Err"); continue; } if (iLen == 0) { cout << "Con Closed" << endl; // 没有接收到数据,证明为客户端关闭连接了,调用close注销句柄,epoll会自动将其对应的Event移除 close(iCliFd); continue; } buf[iLen] = 0; cout << "Recv Info:" << buf << endl; // 收到数据后,将这个客户端连接注册为EPOLLOUT,在缓冲区不满时会触发写事件 sEvent.data.fd = iCliFd; sEvent.events = EPOLLOUT|EPOLLET; epoll_ctl(iEpollFd, EPOLL_CTL_MOD, iCliFd, &sEvent); } else if (sEvents[i].events & EPOLLOUT) { // C.客户端连接写事件 cout << "[EPOLLOUT]" << endl; // 调用send发送数据给客户端 if (send(iCliFd, "Hello New Cli!", 14, 0) < 0) { cout << "Send Err, Info:" << strerror(errno) << endl; continue; } else { cout << "Send Suc" << endl; } // 发送完数据后,再将这个客户端连接注册给EPOLLIN,等待客户端反应 sEvent.data.fd = iCliFd; sEvent.events = EPOLLIN|EPOLLET; epoll_ctl(iEpollFd, EPOLL_CTL_MOD, iCliFd, &sEvent); } } } return 0; }用Python写一个简答的TCP客户端来测试(tcpCli.py):
import socket if __name__=='__main__': addr = ('127.0.0.1', 8888); sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM); sock.connect(addr); sock.send("Hello Libevent, I`m Cli A"); buf = sock.recv(1500); print buf sock.close()服务端编译并运行:
[yiran@localhost epoll]$ g++ -o svr epollSvr.cpp [yiran@localhost epoll]$ ./svr New Con From 127.0.0.1:63937 [EPOLLIN] Recv Info:Hello Libevent, I`m Cli A [EPOLLOUT] Send Suc [EPOLLIN] Con Closed客户端运行:
[yiran@localhost epoll]$ python tcpCli.py Hello New Cli!
这里简单分析下:
1、当客户端调用sock.connect( ),触发epoll事件,服务端if (sEvents[i].data.fd == iSvrFd){ }成立,建立新连接,输出“New Con From 127.0.0.1:63937”
2、当客户端调用sock.send( ),触发epoll事件,服务端if (sEvents[i].events & EPOLLIN){ }成立,读取客户端发送数据,输出“[EPOLLIN] Recv Info:Hello Libevent, I`m Cli A”
3、服务端在第2步时注册了EPOLLOUT,当写缓冲没有满时,会自动触发epoll事件,服务端if (sEvents[i].events & EPOLLOUT) { },向客户端发送数据,输出“[EPOLLOUT] Send Suc”
4、当客户端调用sock.close( ),触发epoll事件,服务端if (sEvents[i].events & EPOLLIN){ }成立,读取不到客户端发送数据,证明为客户端关闭连接了,所以注销句柄,输出“[EPOLLIN] Con Closed”
// =============================================================================================
* epoll的2种触发模式
1、水平模式:Level Triggered(LT),默认工作方式。支持阻塞和非阻塞Socket。这种模式下,epoll会告诉你某个句柄有IO事件,如果你没有处理(epoll_wait()出来之后没有accept或者read等),其会一直通知下去
2、边缘触发:Edge Triggered(ET),高速工作方式。只支持非阻塞Socket(为什么?)。这种模式下,epoll只会将句柄的IO事件通知你一次,如果你没有处理,这个IO事件将会丢失
* epoll相关的数据结构
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;其中events为事件类型,为下面几个宏的集合:
EPOLLIN :文件描述符可以读(包括客户端connect,send/sendto,close);
EPOLLOUT:文件描述符可以写;
EPOLLPRI:文件描述符有紧急的数据可读;
EPOLLERR:文件描述符发生错误;
EPOLLHUP:文件描述符被挂断;
EPOLLET: 将epoll设为ET触发模式,因为默认为LT触发。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
比如你要以ET触发模式监听某句柄的读事件,则设events=EPOLLIN|EPOLLET
* epoll的3个相关函数
epoll的API还是很简单的,只有3个函数,epoll_create( ) / epoll_ctl( ) / epoll_wait( )
1、epoll_create(int maxfds):创建一个epoll句柄,maxfds为这个epoll支持的最大句柄数
2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event):事件控制函数,用来注册/修改/删除事件
第一个参epfd为调用epoll_create( )创建的epoll句柄
第二个参op为事件类型,EPOLL_CTL_ADD为注册,EPOLL_CTL_MOD为修改,EPOLL_CTL_DEL为删除
第三个参fd为要监听的句柄
第四个参event为这个监听事件的具体信息,如要监听这个句柄的读事件还是写
尽量少地调用epoll_ctl,防止其所引来的开销抵消其带来的好处。有的时候,应用中可能存在大量的短连接(比如说Web服务器),epoll_ctl将被频繁地调用,可能成为这个系统的瓶颈
3、int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout):调用一次,epoll就查询当前的网络接口,检测在epoll注册的所有句柄有没有IO事件发生
第一个参epfd为epoll句柄
第二个参events作为一个回参,存放epoll检测到有IO事件发生的句柄信息
第三个参maxevents为第二个参events的长度
第四个参timeout为调用epoll_wait( )的超时时间,单位为ms(0表示立即返回,即非阻塞;-1表示阻塞)
* EPOLLOUT事件什么时候被触发?
这个问题是我一开始学习epoll时有些不解的,因为客户端给服务端发送数据,服务端的接收缓冲区有数据可读而触发EPOLLIN事件。但是写事件是服务端的一个主动行为,服务端怎么主动自己触发自己?
后来发现这个问题其实很简单,就是只要“缓冲区不满”,服务端能写数据,就会一直触发EPOLLOUT。
* 怎么判断客户端连接结束并将对应句柄从epoll中删去?
1、当客户端断开连接,会触发EPOLLIN事件。所以如果在EPOLLIN逻辑里,read返回0(读不到数据,且没有出错),可判断为客户端连接结束。
2、只要将客户端对应的句柄close( )掉,epoll自动会将其从监听句柄中删去。
相关文章推荐
- Linux 与 Windows 对UNICODE 的处理方式
- Ubuntu12.04下QQ完美走起啊!走起啊!有木有啊!
- 解決Linux下Android开发真机调试设备不被识别问题
- Ubuntu Linux使用体验
- c语言实现hashmap(转载)
- Linux 信号signal处理机制
- linux下mysql添加用户
- 关于指针的一些事情
- Scientific Linux 5.5 图形安装教程
- 基于 Linux 集群环境上 GPFS 的问题诊断
- 谁是桌面王者?Win PK Linux三大镇山之宝
- vivi下重新调整分区
- Linux VS Unix:Linux欲一统天下 Unix不死
- linux下设定环境变量
- Linux下修改MySQL编码的方法
- Linux串口通信
- 从Windows系统下访问Linux分区相关软件
- 看看我的Ubuntu Linux截图
- ARM Linux系统启动