epoll简介和使用

        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自动会将其从监听句柄中删去。



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值