linux下的I/O复用模型之epoll详解

  1. epoll模型函数原理和使用
    epoll的监听集合实质是一棵红黑树
头文件:
#include <sys/epoll.h>

(1)int epfd = epll_create(int size)
参数:
size:树节点的数量
		可以用命令查看进程能够打开的最大数目的文件描述符
		xw@ubuntu:~$ cat /proc/sys/fs/file-max
		394030
返回值:成功返回一个非负的指向树的文件描述符,失败返回-1。

(2)epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)
参数:
epfd:函数 epll_create(int size)的返回值
op:系统指定的操作类型,负责树节点的增删改
	(a)添加:EPOLL_CTL_ADD
	(b)修改:EPOLL_CTL_MOD
	(c)删除:EPOLL_CTL_DEL
fd:操作的文件描述符
event:指定内核要监听事件,它是struct epoll_event结构类型的指针
struct epoll_event { 
    uint32_t     events;      /* Epoll events */
    epoll_data_t data;        /* User data variable */
};
	其中events可以是以下几个宏的集合:
	EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
	EPOLLOUT:表示对应的文件描述符可以写;
	EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
	EPOLLERR:表示对应的文件描述符发生错误;
	EPOLLHUP:表示对应的文件描述符被挂断;
	EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,默认的是水平触发(Level Triggered)。
	EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听
				 这个socket的话,需要再次把这个socket加入到EPOLL队列里
返回值:成功返回0,失败返回-1。

(3)int epoll_wait(int epfd, struct epoll_event *events, 
					int maxevents, int timeout);
参数:
	(1)监听的树地址
	(2)就绪队列的首地址
			就绪队列中传出就绪的socket集合,用户只需要遍历这些并处理,
			而不用像select那样只知道就绪socket的个数去自己校验。
			这也是epoll模型不需要传出参数的原因
	(3)就绪最大数量
	(4)超时事件(工作方式)
			(a)-1:阻塞
			(b)0:立即返回(轮询)非阻塞,
			(3)>0:定时等待
返回值:成功返回就绪的文件描述符个数,失败返回-1,时间超时返回0

epoll工作模式:
	epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。
	修改工作模式:
	struct epoll_event ev;
	ev.data.fd = serverfd;
	ev.events |= EPOLLET;
	修改套接字属性为非阻塞
	(a)可以在创建的时候初始化
		socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
	(b)可以自己通过函数修改
		fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK);
		或者
		ioctl(sockfd, FIONBIO, 1);
		
	LT模式是默认模式,LT模式与ET模式的区别如下:

    LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,
    		应用程序可以不立即处理该事件。下次调用epoll_wait时,
    		会再次响应应用程序并通知此事件。
			只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,
			提醒用户程序去操作。
			
    ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,
    		应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,
    		不会再次响应应用程序并通知此事件。
    		所以在ET模式下,read一个fd的时候一定要把它的buffer读光,
    		也就是说一直读到read的返回值小于请求值,或者遇到EAGAIN错误。

LT模式:有较大的系统开销,但是可以保证数据的完整性
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。

ET模式:开销更小,但是需要用户在操作读取事件数据时自己保障数据的读取完整性
ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

  1. epoll服务器代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/select.h>
#include<arpa/inet.h>
#include<sys/epoll.h>

#define MAXEVENTS	5000

int main()
{
	//网络初始化
	char recvbuf[1024];
	char ip[16];
	int  ret;
	int  epfd;
	int  nfds;
	int  clientfd;
	struct epoll_event ev,ready[MAXEVENTS];

	struct sockaddr_in addrserver,addrclient;
	bzero(&addrserver,sizeof(addrserver));
	bzero(&recvbuf,sizeof(recvbuf));
	bzero(&ip,sizeof(ip));

	addrserver.sin_family = AF_INET;
	addrserver.sin_port = htons(27015);
	addrserver.sin_addr.s_addr = htonl(INADDR_ANY);

	//创建socket
	int serverfd = socket(AF_INET,SOCK_STREAM,0);
	if(serverfd == -1){
		printf("create socket error\n");
		return 1;
	}
	//绑定
	ret = bind(serverfd,(struct sockaddr*)&addrserver,sizeof(addrserver));
	if(ret == -1)
	{
		printf("bind error\n");
		close(serverfd);
		return 1;
	}
	//监听
	ret = listen(serverfd,128);
	if(ret == -1)
	{
		printf("listen error\n");
		close(serverfd);
		return 1;
	}

	//创建epoll监听树
	epfd = epoll_create(MAXEVENTS);
	if(epfd == -1)
	{
		printf("epoll_create error\n");
		return -1;
	}

	//准备树节点结构
	ev.data.fd = serverfd;
	ev.events = EPOLLIN;
	//添加监听节点
	ret = epoll_ctl(epfd,EPOLL_CTL_ADD,serverfd,&ev);
	if(ret == -1)
	{
		printf("epoll_ctl: add serverfd error\n");
		return -1;
	}

	printf("epoll Server Runing.....\n");
	
	while(1)
	{	
		nfds = epoll_wait(epfd,ready,MAXEVENTS,-1);
		if(nfds == -1)
		{
			printf("epoll_wait failed\n");
			return -1;
		}
		
		for(int i = 0; i < nfds; ++i)
		{
			//监听客户端建立连接
			if(ready[i].data.fd == serverfd)
			{
				socklen_t  size = sizeof(addrclient);
				clientfd = accept(serverfd,(struct sockaddr*)&addrclient,&size);
				if(clientfd == -1)
				{
					printf("accept error\n");
					return -1;
				}
				//添加到树中
				ev.data.fd = clientfd;
				ev.events = EPOLLIN;
				ret = epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);
				if(ret == -1)
				{
					printf("epoll_ctl: add clientfd error\n");
					return -1;
				}
			}
			else	//数据传输 
			{
					ret = recv(ready[i].data.fd,recvbuf,sizeof(recvbuf),0);
					if(ret > 0)
					{
						printf("recv data is:%s\n",recvbuf);
						continue;
					}
					else if(ret == 0)
					{
						printf("client normal closed...\n");
						close(ready[i].data.fd);
						epoll_ctl(epfd,EPOLL_CTL_DEL,ready[i].data.fd,NULL);
						break;
					}
					else{
						printf("recv error\n");
						close(ready[i].data.fd);
						epoll_ctl(epfd,EPOLL_CTL_DEL,ready[i].data.fd,NULL);
						break;
					}
			}

		}

	}
	close(serverfd);
	return 0;
}
  1. 测试结果

  2. 模型评价
    优点:
    (1) 没有最大并发连接的限制,能打开的文件描述符的上限远大于1024(1G的内存上能监听约10万个端口),真正意义上突破了文件描述符的个数限制。上限是最大可以打开文件的数目,一般来说这个数目和系统内存关系很大
    (2)epoll并采用轮询的方式,也就是没有使用内核IO设备队列,而是自己创建一个监听队列,使用的是异步抛出手段(事件/通知/回调),而是通过每个fd定义的回调函数来实现的,只有就绪的fd才会执行回调函数。 所以 epoll 不会随着FD数目的增加效率下降。只有活跃可用的fd才会调用callback函数。即 Epoll 最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。
    (3) 为了避免epoll大量的就绪拷贝开销,采用mmap内存共享映射的技术。利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
    (4)epoll的监听集合不会产生重复拷贝挂载现象,每一个监听的socket只会拷贝挂载一次
    缺点:如果对epoll频繁的插入,删除操作,可能会导致维护数据结构的开销越来越大,处理效率降低(甚至会低于select和poll)。

参考资料:
1.Linux IO模式及 select、poll、epoll详解
2.Linux网络编程—I/O复用模型之epoll
3.服务器编程心得(四)—— 如何将socket设置为非阻塞模式

linux学习笔记 文章被收录于专栏

linux学习笔记

全部评论

相关推荐

点赞 评论 收藏
分享
走不到的路就这样算了吗:大佬硬气
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务