epoll系统调用

        epoll是Linux系统特有的I/O复用函数。它和selectpoll有很大的区别,首先它使用的是一组函数来完成任务而不是一个函数;其次,epoll是将用户关心的文件描述符的事件放在内核中的一个事件表中,从而无须像select和poll每次重复传入文件描述符或者事件集,但是epoll需要一个额外的文件描述符用来唯一标识内核中的事件表。epoll针对的是同一时刻成千上万的文件描述符需要监听的情况,毕竟像select和poll那种轮询的方式来监听文件描述符的话,效率是极其低下的。

epoll有两种模式,一种是默认工作模式LT模式,另外一种是高效工作模式ET模式。

LT模式:某一个文件描述符上一旦有数据就绪,就提醒应用程序,一次没有处理完或者未处理的话,会继续提醒,直到处理完。

ET模式:某一个文件描述符上一旦有数据,就提醒应用程序一次,数据没有处理或者没有处理完全,下一轮epoll也不会再提醒,除非是第二波的数据到了,再提醒;因此epoll在ET模式下,数据就绪后只提醒应用程序一次。

先来说一下LT模式,不论是LT模式还是ET模式都会使用以下三个函数实现操作:

int epoll_create(int size);//创建内核事件表;size的作用只是告诉内核事件表的大小,其返回值是用来唯一标识该事件表的文件描述符,即下面函数的第一个参数epfd。

int epoll_ctl(int epfd,int op,int fd,struct epoll_event*event);//添加文件描述符&移除文件描述符;第二个参数是指定操作类型,一般有三种操作:

EPOLL_CTL_ADD: 往事件表中注册fd上的事件

EPOLL_CTL_DEL :删除fd上注册的事件

EPOLL_CTL_MOD:修改fd上的注册事件

event指定事件类型,其本身是一个结构体:

struct epoll_event
{   
    unsigned int event;
    epoll_data_t data;
 };

int epoll_wait(int epfd,struct epoll_event*events,int max,int timeout);//检查就绪文件描述符,返回给应用程序,只返回就绪的文件描述符。该函数返回成功时,返回就绪文件描述符的个数,失败则返回-1,max指定监听的最大个数,timeout是超时设定,单位是毫秒。

下面利用这些函数写一个简单的服务器,感受一些效果。

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/select.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/epoll.h>

#define MAX 10
int sock_create();

void epoll_add(int epfd,int fd)
{
	struct epoll_event ev;
	ev.events = EPOLLIN;
	ev.data.fd = fd;
	if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) == -1)
	{
		perror("epoll_ctl error");
	}
	
}
void epoll_del(int epfd,int fd)
{
	if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL) == -1)
	{
		perror("epoll_ctl error");
	}
}

int main()
{
	int sockfd = sock_create();

	int epfd = epoll_create(MAX);
	assert(epfd != -1);
	
	epoll_add(epfd,sockfd);

	struct epoll_event fds[MAX];

	while(1)
	{
		int n = epoll_wait(epfd,fds,MAX,5000);
		if(n == -1)
		{
			perror("epoll_wait error");
			continue;
		}
		if(n == 0)
		{
			printf("time out\n");
			continue;
		}
		else
		{
			int i = 0;
			for(;i<n;i++)
			{
				if(fds[i].events & EPOLLIN)
				{
					if(fds[i].data.fd == sockfd)
					{
						struct sockaddr_in caddr;
						int len = sizeof(caddr);
						int c = accept(fds[i].data.fd,(struct sockaddr*)&caddr,&len);
						assert(c != -1);
						epoll_add(epfd,c);
						continue;
					}
					else
					{
						char buff[128]={0};
						if(recv(fds[i].data.fd,buff,1,0)>0)
						{
							printf("read:%s\n",buff);
							memset(&buff,0,128);
							send(fds[i].data.fd,"ok",2,0);
							sleep(2);
							continue;
						}
						else
						{
							printf("%d client over!\n",fds[i].data.fd);
							int fd_ = fds[i].data.fd;
							epoll_del(epfd,fds[i].data.fd);
							close(fd_);
							continue;
						}
					}
				}
			}
		}
	}
}

int sock_create()
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	assert(sockfd != -1);

	struct sockaddr_in saddr;
	memset(&saddr,0,sizeof(saddr));
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(6000);
	saddr.sin_addr.s_addr = inet_addr("192.168.31.17");

	int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
	assert(res != -1);

	listen(sockfd,5);
	return sockfd;
}
另外再写一个通用的客户端向该服务器发送数据,客户端由用户从键盘接收数据,发送到服务器,服务器接收数据并打印这些数据,并发送确认数据"ok",表示已接收到来自客户端的数据。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<assert.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<pthread.h>

int main()
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	assert(sockfd != -1);
	
	struct sockaddr_in saddr;
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(6000);
	saddr.sin_addr.s_addr = inet_addr("192.168.31.17");
	
	int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
	assert(res != -1);
	
	while(1)
	{
		printf("input:\n");
		char buff[128]={0};
		fgets(buff,128,stdin);
		if(strncmp(buff,"end",3) == 0)
		{
			break;
		}
		send(sockfd,buff,strlen(buff),0);
		memset(buff,0,128);
		recv(sockfd,buff,127,0);
		printf("buff = %s\n",buff);
	}
	close(sockfd);
}
这里只说明一次未处理完来自客户端的数据,服务器的处理方式,例如服务器一次只接收一个字符。


测试用例结果显示,服务端对于来自客户端的数据,分六次读取完毕,并且向客户端发送了一个确认信息"ok",这证明了前面我说的epoll的LT模式的特点是对的,并且如果客户端在服务端未读完数据时就关闭连接,服务端会依次读完数据并打印客户端结束连接的提醒。


隔了几秒后:


以上就是epoll LT模式,接下来说ET模式:

       ET模式是epoll的高效工作模式,前面说了ET模式的特点,但是也有个问题:那么一次未处理完的数据,下次读到的还是有效的吗?若是,谁知道下次是什么时候,难道我就为了处理完第一次的数据我就得多发n次消息,才能处理?

开玩笑,若是这么理解的话,epollET模式的作用还有个喵啊!在这里我们能想到的是,我为何不一次就把它处理完,省的麻烦,若是这样的话,那跟使用多线程、多进程处理这类问题还有个毛区别,万一没有数据,阻塞了咋办,IO复用函数实现的服务器只有一个主线程啊,咩。所有引入了一个新的概念“非阻塞的套接字”,这样就保证一次读完数据而不会阻塞服务端。那下面验证一下呗。下面是ET模式的源码:

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>
#include<sys/select.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<fcntl.h>

#define MAX 10
int sock_create();

void setnonblock(int fd)//设置非阻塞文件描述符
{
	int oldfl = fcntl(fd,F_GETFL);
	int newfl = oldfl | O_NONBLOCK;
	if(fcntl(fd,F_SETFL,newfl) == -1)
	{
		perror("fcntl error");
	}
}


void epoll_add(int epfd,int fd)
{
	struct epoll_event ev;
	ev.events = EPOLLIN|EPOLLET;
	ev.data.fd = fd;
	if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) == -1)
	{
		perror("epoll_ctl error");
	}
	setnonblock(fd);//将fd设置为非阻塞文件描述符
}
void epoll_del(int epfd,int fd)
{
	if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL) == -1)
	{
		perror("epoll_ctl error");
	}
}

int main()
{
	int sockfd = sock_create();

	int epfd = epoll_create(MAX);
	assert(epfd != -1);
	
	epoll_add(epfd,sockfd);

	struct epoll_event fds[MAX];

	while(1)
	{
		int n = epoll_wait(epfd,fds,MAX,5000);
		if(n == -1)
		{
			perror("epoll_wait error");
			continue;
		}
		if(n == 0)
		{
			printf("time out\n");
			continue;
		}
		else
		{
			int i = 0;
			for(;i<n;i++)
			{
				if(fds[i].events & EPOLLIN)
				{
					if(fds[i].data.fd == sockfd)
					{
						struct sockaddr_in caddr;
						int len = sizeof(caddr);
						int c = accept(fds[i].data.fd,(struct sockaddr*)&caddr,&len);
						assert(c != -1);
						epoll_add(epfd,c);
						continue;
					}
					else
					{
						while(1)//这里跟LT模式有了区别了
						{
							char buff[128]={0};
							int num = recv(fds[i].data.fd,buff,1,0);
							if(num == -1)
							{
								send(fds[i].data.fd,"ok",2,0);
								break;
							}
							else if(num == 0)
							{
								int fd = fds[i].data.fd;
								epoll_del(epfd,fds[i].data.fd);
								close(fd);
							}
							else
							{
								printf("read(%d):%s\n",fds[i].data.fd,buff);
							}
						}
					}
				}
			}
		}
	}
}

int sock_create()
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	assert(sockfd != -1);

	struct sockaddr_in saddr;
	memset(&saddr,0,sizeof(saddr));
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(6000);
	saddr.sin_addr.s_addr = inet_addr("192.168.31.17");

	int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
	assert(res != -1);

	listen(sockfd,5);
	return sockfd;
}

看起来跟前面的LT没什么区别,但是效率已经高了很多了,如果是默认的文件描述符的话,你最多也只能读到一个'h'就阻塞了,总的来说,epoll的ET模式是综合了多线程多进程读数据的方式,又包含select和poll那样处理文件描述符的方式,及两家之长,在Linux环境下,已经是一种很高效的方法了。

好了,以上就是我的理解,也许太基础,但会随着项目的深入,我对epoll的理解会更深一个层次。

杀青!!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值