您的位置:首页 > 运维架构 > Linux

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):

#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 epoll socket c++