I/0多路复用之epoll

epoll介绍


epoll的实现和select与poll的实现有很大的差异,epoll不像select和poll一样通过一个系统调用来完成任务,通过一组函数,epoll把用户关心的文件描述符的事件放在内核的一个事件表中,这样就不用每次都进行向内核传递文件描述符了。epoll使用一个额外的文件描述符来表示内核的事件表,所以这里第一个函数epoll_create就是做这个的。

epoll_create

int epoll_create(int size);
 
 
  • 1
  • 1

size现在并不起作用,给内核提示,告诉内核事件表有多大。这个函数返回的文件描述符用作其他的epoll相关函数的参数。指向要访问的内核事件表。

然后,我们又有一个函数就是epoll_ctl。

epoll_ctl

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

这个函数用来进行向事件表注册事件,参数epfd就是我们epoll_create函数的返回值, 
op是操作类型,有三种。

op参数 说明
EPOLL_CTL_ADD 向事件表中注册fd上的事件
EPOLL_CTL_MOD 修改fd上的注册事件
EPOLL_CTL_DEL 删除fd上的注册事件

第三个参数是epoll_event结构指针类型,我们首先来看epoll_event结构。

struct epoll_event {
               uint32_t     events;    /* Epoll events */
               epoll_data_t data;      /* User data variable */
           };

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

这里的events也是一个即是输入型参数,又是输出型参数,你所关心的监听事件,你jinx设置events,然后如果发生事件,你也可以从events进行获得。 
events的参数和poll中的类似,前面加了E。

events参数 说明
EPOLLIN 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT 表示对应的文件描述符可以写
EPOLLPRI 表示对应的文件描述符有紧急-的数据可读(这里应该表示有带外数据到来)
EPOLLERR 表示对应的文件描述符发生错误
EPOLLHUP 表示对应的文件描述符被挂断
EPOLLET 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里1

后面两个参数EPOLLET和EPOLLONESHOT是epoll独有的,它们提供给了epoll高效的操作,后续进行介绍。

data是一个结构:


 typedef union epoll_data {
               void    *ptr;
               int      fd;
               uint32_t u32;
               uint64_t u64;
           } epoll_data_t;

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这个共用体提供了一些成员来供用户使用。一般来说使用最多的是fd,它指定事件所属的目标文件描述符。ptr成员可以用来指定与fd相关的用户数据。但是这是一个联合,所以我们想要实现一个快速的数据访问,我们可以采取其他办法,比如说我们创建一个结构体,让ptr指向这个结构体,这个结构体当中保存这数据和fd。

最后,我们来说一下epoll_wait

epoll_wait


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

 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

epoll来进行等待文件描述符的事件。这个函数成功的话,返回就绪文件描述符的数量,失败返回-1.

第一个参数epfd,就是epoll_create所得到的返回值,也就是内核事件表。然后第二个参数是结构体数组,这个结构体元素的结构我们在上面讲epoll_ctl的时候已经说过。这个数组用于输出epoll_wait检测到的就绪事件,并不是一个即输入,又输出的参数,这样就可以大大地提高了程序找到就绪文件描述符的效率。maxevents指的是监听事件个数,也就是数组的大小。最后的timeout,设置超时时间,参数与poll的是一样的。

epoll的实际上,当你进行create的时候,这个时候内核进行准备你要监控的文件描述符,然后当是哟个epoll_ctl的使用,其实就是往内核中继续加入新的文件描述符。

代码如下:

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

static void Usage(const char *str)
{
	printf("Usage:%s [serv_ip][serv_port]\n",str);
}

typedef struct fd_buf  //struct epoll_event ev  ev.data.ptr 指向这个结构体。
{
	int fd;
	char buf[10240]; //每一个套接字都有自己的缓冲区。
}fd_buf_t,*fd_buf_p;


static void* alloc_fd_buf(int fd)
{
	fd_buf_p tmp = (fd_buf_p)malloc(sizeof(fd_buf_t));
	if(tmp == NULL)
	{
		perror("malloc");
		return NULL;
	}
	tmp->fd = fd;
	return tmp;
}


static int startup(const char *ip,int port)
{
	int listen_socket = socket(AF_INET,SOCK_STREAM,0);
	if(listen_socket < 0)
	{
		perror("socket");
		exit(1);
	}
	int op = 1;
	setsockopt(listen_socket,SOL_SOCKET,SO_REUSEADDR,&op,sizeof(op)); //端口复用

	struct sockaddr_in serv_addr;
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = inet_addr(ip);
	serv_addr.sin_port = htons(port);

	int ret = bind(listen_socket,(struct sockaddr*)&serv_addr,sizeof(serv_addr)  );
	if(ret < 0)
	{
		perror("bind");
		exit(2);
	}

	ret = listen(listen_socket,128);
	if(ret < 0)
	{
		perror("listen");
		exit(3);
	}
	return listen_socket;
}

int main(int argc,char *argv[])
{

	if(argc != 3)
	{
		Usage(argv[0]);
		return 1;
	}

	int listen_sock = startup(argv[1],atoi(argv[2]));

	int epollfd = epoll_create(256);//建议操作系统创建一个有256个节点的红黑树。
	if(epollfd < 0)
	{
		perror("epoll_create");
		close(listen_sock);
		return 5;
	}

	struct epoll_event ev;
	ev.events = EPOLLIN; //监听listen_socket的可读事件。
	ev.data.ptr = alloc_fd_buf(listen_sock); //每一个fd都分配自己的空间。

	int ret = epoll_ctl(epollfd,EPOLL_CTL_ADD,listen_sock,&ev);//将listen_sock加入到红黑树中,并且监听它的可读事件。


	int nums = 0;
	struct epoll_event evs[64];
	int timeout = -1;

	while(1)
	{
		switch(  (nums = epoll_wait(epollfd,evs,64,timeout) )  )  //红黑树中就绪的节点放入循环队列中,把队列中的事件放入epoll_event数组中。它不会修改红黑树的节点和定时器,所以不需要每次都重新初始化。
		{
			case -1:
				perror("epoll_wait");
				break;
			case 0:
				printf("timeout....!\n");
				break;
			default:
				{
					int i = 0;
					for(; i < nums; ++i) //遍历数组,这个数组的元素都是就绪的。
					{
						fd_buf_p fp =  (fd_buf_p)evs[i].data.ptr; //ptr指向一个结构,这个结构中包含了一个文件描述符和一块空间。

						if(fp->fd == listen_sock &&(evs[i].events & EPOLLIN))
						{
							struct sockaddr_in clie_addr;
							socklen_t len = sizeof(clie_addr);

							int new_sock = accept(listen_sock, (struct sockaddr*)&clie_addr,&len);
							if(new_sock < 0)
							{
								perror("accept");
								continue;
							}
							printf("get a new client\n");

							struct epoll_event temp;
							temp.events = EPOLLIN;
							temp.data.ptr = alloc_fd_buf(new_sock); //将连接套接字加入到红黑树中
							epoll_ctl(epollfd,EPOLL_CTL_ADD,new_sock,&temp);
						}//if
						else if(fp->fd != listen_sock)
						{
							if(evs[i].events & EPOLLIN) //可读事件。
							{
								ssize_t s = read(fp->fd,fp->buf,sizeof(fp->buf)-1); //
								if(s > 0)
								{
									fp->buf[s] = 0; //从标准输入中读取s需要减一,其他情况下不需要。
									printf("client say:%s\n",fp->buf);
									//读完后让这个套接字成为写状态。
									struct epoll_event temp;
									temp.events = EPOLLOUT;
									temp.data.ptr = fp;

									epoll_ctl(epollfd,EPOLL_CTL_MOD,fp->fd,&temp);
								}
								else if (s <= 0)
								{
									close(fp->fd);
									epoll_ctl(epollfd,EPOLL_CTL_DEL,fp->fd,NULL);
									free(fp);
								}

							} //if 可读事件。
							else if(evs[i].events & EPOLLOUT) //可写事件。
							{
								const char *msg = "HTTP/1.0 200 OK\r\n\r\n<html><h1> hello epoll!</h1></html>";
								write(fp->fd,msg, strlen(msg));

								close(fp->fd);
								epoll_ctl(epollfd,EPOLL_CTL_DEL,fp->fd,NULL);
								free(fp);
							}
						} 
					} //for finish;
					break;
				} //default finish
		}
	}

	return 0;
}

截图:

(1)首先关闭防火墙:


(2)服务器启动:


(3)浏览器访问


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值