EPOLL使用的简单总结

0. 为什么要用epoll

既然用到epoll,一定对select和poll有一定的了解。
Select需要与fd_set结构体配合使用,并在用户空间维护一个客户端描述符,且管理句柄时有数目的限制。
Poll解决了句柄数目的限制(链表实现),同时维护一个pollfd结构体的客户端事件的集合。

这来俩性能局限点为:
Select和POLL都会遍历整个集合来确定活跃描述符
与内核交互时会把所有句柄拷贝到内核

注意的是:
服务器性能四大杀手:
1.数据拷贝-> 缓存方案
2.环境切换(线程切换)->单核单线程,多核多线程
3.内存分配->内存池
4.锁竞争->减少锁的使用

Poll每次需要从用户态将所有的句柄复制到内核态,如果以万计的句柄会导致每次都要copy几十几百KB的内存到内核态,非常低效。使用epoll时你只需要调用epoll_ctl事先添加到对应红黑树,真正用epoll_wait时不用传递socket句柄给内核,节省了拷贝开销。

以上此段出自阿里云《epoll全面讲解:从实现到应用》https://www.aliyun.com/jiaocheng/122174.html

Epoll在内核的实现使用了mmap共享内存,红黑树和锁,所以在一定条件下提升机器的性能:
大量链接的/不是所有的句柄都很活跃 条件下使用epoll

1. 为什么要使用非阻塞模式

ET模式需要非阻塞。
为此我们需要知道什么是阻塞模式,非阻塞模式,IO复用模型。
此外,在服务器程序中发生阻塞一般是读写数据和accept等待链接的时候。

以下图和思想,来源于《Unix网络编程 第二版》第一卷 第二部分 第六章 第二节

阻塞模式:
阻塞IO模型
正如原文所说,一开始写的网络编程代码都是阻塞模式,直观一点的意思就是没有用到select/poll,直接使用socket-> sockaddr_in ->bind->listen->while(1)->accept模型的简单回射服务器就是阻塞IO模型应用。此模型的局限是一个线程或者进程只能同时处理一个描述符。

非阻塞模式:
非阻塞IO模型
也就是应用层一直检查内核是否准备好数据,直到完成。可以做个简单的实验,就上面说过的回射服务器,直接设置成非租塞,accept会一直返回-1。原因是一直在等待链接,当链接到来读写完数据,再次疯狂返回-1。

IO复用模式:
IO复用模型
如图,IO复用其实就是select/poll/epoll这类的函数,它们们帮我们完成了内核的监控,并可以监控多个,当内核某个IO准备好后通知我们,我们在调用。与上面的非阻塞模式配合使用就不会反会-1的错误(当数据准备好后再accept,举例select也就是if (pollfds[0].revents & POLLIN){… accept …})。

我在使用第一次使用epoll时候(就是写这完文档的前一天)使用的是<非阻塞+IO复用+LT模式>,其实LT模式下非阻塞性能不高,但是好写。
之后会改ET。
先放个图。
epoll触发模式

图片来源图片来源CSDN《epoll EPOLLL、EPOLLET模式与阻塞、非阻塞》https://blog.csdn.net/zxm342698145/article/details/80524331

2. epoll使用(c++,面向过程)

先说一下用epoll和不用IO复用网络服务器编程的区别
首先是阻塞的编程流程(个人总结不是很严谨):
阻塞IO编码流程

然后就是epoll 的IO复用编程模型:
epoll的IO复用模型
代码中的体现如下:

int main()
{
	/*Socket(AF_INET, SOCK_STREAM, 0),我这里设置了非阻塞模式,下面的accept4也是。*/
	int listenfd;
	listenfd = Socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); // fei zu se IO fu yong
	/*设置服务器的sockaddr_in结构体,IPv4,当前地址,8000端口*/
	struct sockaddr_in serveraddr;
	bzero(&serveraddr, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons(8000);
	/*重连处理*/
	int opt = 1;
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	/*Bind绑定描述符和服务器结构体*/
	Bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
	/*Listen监听描述符*/
	Listen(listenfd, 20);
	/*准备客户端sockaddr_in结构体,以及accept的返回值*/
	struct sockaddr_in clientaddr;
	socklen_t clientlen;
	int connfd;

	/*准备epoll的epoll_event结构体集,用的是c++的向量,为了方便*/
	typedef std::vector<struct epoll_event> EpollList;     
	/*epoll_create1(EPOLL_CLOEXEC)生成用于处理accept的epoll专用的文件描述符,创建一个epoll的句柄*/
	int epollfd;
	epollfd = epoll_create1(EPOLL_CLOEXEC);
	//Creates a handle to epoll, the size of which tells the kernel how many listeners there are.
	/*设置epoll_event结构体监听事件,epoll_event结构体的变量,epfd用于注册事件*/
	struct epoll_event epfd;
	epfd.data.fd = listenfd;
	epfd.events = EPOLLIN/*| EPOLLET */;
	/*epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &epfd);
	epollfd为epoll_create1返回,epfd为epoll_event结构体监听事件的结构体
	epoll的事件注册函数,它不同与select()是在监听事件时(epoll使用epoll_wait监听)告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型*/
	epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &epfd);
	/*设置epoll_event结构体集的大小*/
	EpollList events(16);//You can listen for 16 at first

	int nready;//活跃描述符个数

	while(1)
	{	
		/*nready = epoll_wait(epollfd, &*events.begin(), static_cast<int>(events.size()), -1); 
			等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,
			这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1是永久阻塞)。
			该函数返回需要处理的事件数目,如返回0表示已超时。*/
		nready = epoll_wait(epollfd, &*events.begin(), static_cast<int>(events.size()), -1);
		if (nready == -1)//出错处理
		{
			if(errno == EINTR) 
				continue;
			perror("epoll_wait");
		}
		if(nready == 0) //如果没有活跃的重来
			continue;

		if ((size_t)nready == events.size())//如果结构体集不够用了,倍增
		{
			events.resize(events.size() * 2);
		}
		/*遍历返回的活跃描述符for(int  i=0; i < nready; ++i)*/
		for(int  i=0; i < nready; ++i)
		{
			/*if (events[i].data.fd == listenfd)监听活跃*/
			if (events[i].data.fd == listenfd)
			{
				/*Accept客户端结构体和监听描述符,返回一个客户描述符,accept4比accept定义一个参数*/
				clientlen = sizeof(clientaddr);
				connfd = Accept4(listenfd, (struct sockaddr*)&clientaddr, &clientlen, 
									SOCK_NONBLOCK | SOCK_CLOEXEC);// fei zu se IO fu yong

				std::cout << connfd << "is come!" << std::endl;
				/*有客户访问到来,修改结构体事件,把监听描述符改为客户链接描述符,写入内核*/
				epfd.data.fd = connfd;
				epfd.events = EPOLLIN/* | EPOLLET*/;
				epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &epfd);

			}
			/*if (events[i].events & EPOLLIN)客户描述符活跃,客户链接描述符有可读事件*/
			else if (events[i].events & EPOLLIN)
			{
				connfd = events[i].data.fd;//取出链接描述符使用
				if (connfd < 0)
				{
					continue;
				}
				/*用于读写的准备*/
				char buf[100];
				bzero(buf, sizeof(buf));
				int n;
				if ((n = read(connfd, buf, 100)) > 0)
				{	
					std::cout << "::" << connfd <<" Date: ["<< buf <<"]" << std::endl;
					write(connfd, buf, n);
				}
				/*关闭描述符,就是客户断开连接后处理*/
				else if (n == 0)
				{
					std::cout << connfd << "is go" << std::endl;
					close(connfd);
					epfd = events[i];
					epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, &epfd);
				}

			}
		}

	}

	return 0;
}

3. epoll接口和结构体

Epoll的头文件

#include <sys/epoll.h>

Epoll的函数接口

	Int epoll_create(int size);

参数size为设置可以连接的多少,老的create函数,实例epoll,现在参数size被忽略,大小取决于内核的处理能力。

Int epoll_create1(int flags);

推荐使用的新版本, flags参数的值为EPOLL_CLOEXEC ;

Int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

事件注册函数

  • epfd:epoll_create返回的实例;

  • op:表示动作
    EPOLL_CTL_ADD:注册新的fd到epfd中;
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL:从epfd中删除一个fd;

  • fd:监听的文件描述符;

  • event:通知内核的结构体下页说明

     Int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    

相当于select函数等待事件产生maxevents:通知内核event的大小;timeout:超时时间,-1为永远等待;

EPOLL结构体

Typedef union epoll_date{
		void *ptr;
		int fd;
		unit32_t u32;
		unit64_t u64;
}epoll_data_t

联合体,用户数据变量,一般使用fd文件描述符

Struct epoll_event{
	unit32_t events;
	epoll_data_t date;
}

events可以是以下几个宏的集合:
**EPOLLIN **:表示对应的文件描述符可以读(包括对端SOCKET正常关闭);

EPOLLOUT:表示对应的文件描述符可以写;

EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

EPOLLERR:表示对应的文件描述符发生错误;

EPOLLHUP:表示对应的文件描述符被挂断;

EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。

EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个 socket加入到EPOLL队列里

参考博客
博主:lvyilong316
http://blog.chinaunix.net/uid/28541347.html
epoll专栏,共10篇

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值