优于select的epoll

epoll理解及应用

基于select的I/O复用技术速度慢的原因

①调用select函数后常见的针对所有文件描述符的循环语句

②每次调用select函数时都需要向该函数传递监视对象信息

  只看代码的话很容易认为循环是提高性能的更大障碍,但相比于循环语句,更大的障碍是每次传递监视对象信息:每次调用select函数时向操作系统传递监视对象信息。应用程序向操作系统传递数据将对程序造成很大负担,而且无法通过优化代码解决。

  弥补方法:仅向操作系统传递1次监视对象,监视范围或内容发生变化时只通知发生变化的事项。如Linux下的epoll和Windows下的IOCP

 select的优点是跨平台特性比较好。

epoll的概述

①对于待检测集合select和poll是基于线性方式处理的,epoll是基于红黑树来管理待检测集合的。
②select和poll每次都会线性扫描整个待检测集合,集合越大速度越慢,epoll使用的是回调机制,效率高,处理效率也不会随着检测集合的变大而下降
③select和poll工作过程中存在内核/用户空间数据的频繁拷贝问题,在epoll中内核和用户区使用的是共享内存(基于mmap内存映射区实现),省去了不必要的内存拷贝。
④程序猿需要对select和poll返回的集合进行判断才能知道哪些文件描述符是就绪的,通过epoll可以直接得到已就绪的文件描述符集合,无需再次检测
⑤使用 epoll 没有最大文件描述符的限制,仅受系统中进程能打开的最大文件数目限制

实现epoll时必要的函数和结构体

epoll_create

epoll_create() 函数的作用是创建一个红黑树模型的实例,用于管理待检测的文件描述符的集合。

int epoll_create(int size);

函数参数 size:在 Linux 内核 2.6.8 版本以后,这个参数是被忽略的,只需要指定一个大于 0 的数值就可以了。
函数返回值:
失败:返回 - 1
成功:返回一个有效的文件描述符,通过这个文件描述符就可以访问创建的 epoll 实例了

  该函数和创建套接字的情况相同,也会返回文件描述符。需要终止时,也要调用close函数。

epoll_ctl

epoll_ctl()函数的作用是管理红黑树实例上的节点,可以进行添加、删除、修改操作

epoll方式通过结构体epoll_event将发生变化的文件描述符单独集中到一起

// 联合体, 多个变量共用同一块内存        
typedef union epoll_data {
 	void        *ptr;
	int          fd;	// 通常情况下使用这个成员, 和epoll_ctl的第三个参数相同即可
	uint32_t     u32;
	uint64_t     u64;
} epoll_data_t;

struct epoll_event {
	uint32_t     events;      /* Epoll events */
	epoll_data_t data;        /* User data variable */
};
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

函数参数:
epfd:epoll_create () 函数的返回值,通过这个参数找到 epoll 实例
op:这是一个枚举值,控制通过该函数执行什么操作
        EPOLL_CTL_ADD:往 epoll 模型中添加新的节点
        EPOLL_CTL_MOD:修改 epoll 模型中已经存在的节点
        EPOLL_CTL_DEL:删除 epoll 模型中的指定的节点
fd:文件描述符,即要添加 / 修改 / 删除的文件描述符
event:epoll 事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件,是epoll_event结构体的指针。epoll_event结构体用于保存发生事件的文件描述符集合,也可以在epoll例程中注册文件描述符时,用于注册关注的事件。
        events:委托 epoll 检测的事件(常见)
                EPOLLIN:读事件,接收数据,检测读缓冲区,如果有数据该文件描述符就绪
                EPOLLOUT:写事件,发送数据,检测写缓冲区,如果可写该文件描述符就绪
                EPOLLERR:异常事件

可以通过位或运算同时传递多个上述参数。
        data:用户数据变量,这是一个联合体类型,通常情况下使用里边的 fd 成员,用于存储待检          测的文件描述符的值,在调用 epoll_wait() 函数的时候这个值会被传出。
函数返回值:
失败:返回 - 1
成功:返回 0

从监视对象中删除时,不需要监视信息(事件信息,因此向第四个参数传递NULL)

epoll_ctl(A,EPOLL_CTL_DEL,B,NULL); //从epoll例程中删除文件描述符B

epoll_wait

epoll_wait() 函数的作用是检测创建的 epoll 实例中有没有就绪的文件描述符。

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

函数参数:
        epfd:epoll_create () 函数的返回值,通过这个参数找到 epoll 实例
        events:传出参数,这是一个结构体数组的地址,里边存储了已就绪的文件描述符的信息
        maxevents:修饰第二个参数,结构体数组的容量(元素个数)
        timeout:如果检测的 epoll 实例中没有已就绪的文件描述符,该函数阻塞的时长,单位 ms 毫秒
                0:函数不阻塞,不管 epoll 实例中有没有就绪的文件描述符,函数被调用后都直接返回
                大于 0:如果 epoll 实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数再返回
                -1:函数一直阻塞,直到 epoll 实例中有已就绪的文件描述符之后才解除阻塞
函数返回值:
        成功:
                等于 0:函数是阻塞被强制解除了,没有检测到满足条件的文件描述符
                大于 0:检测到的已就绪的文件描述符的总个数
        失败:返回 - 1

调用此函数时,第二个参数所指缓冲需要动态分配

int event_cnt;
struct epoll_event * ep_events;
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE); //EPOLL_SIZE是宏常量
event_cnt = epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);

echo_epollserv.c:

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

#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *buf);

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	socklen_t adr_sz;
	int str_len, i;
	char buf[BUF_SIZE];

	struct epoll_event *ep_events;
	struct epoll_event event;
	int epfd, event_cnt;

	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));

	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");

	epfd=epoll_create(EPOLL_SIZE);  //创建epoll实例

	//ep_events每次会被覆盖着放入epoll_event,个数为发生变化的文件描述符数量
	ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE); //动态分配epoll_event所需的内存空间

	event.events=EPOLLIN;  //检测读事件
	event.data.fd=serv_sock;	//需要监听的文件描述符
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); //向epfd中添加需要监听的文件描述符

	while(1)
	{   //检测创建的 epoll 实例中有没有就绪的文件描述符,-1表示该函数一直阻塞
		event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);//返回发生变化的文件描述符数量
		if(event_cnt==-1)
		{
			puts("epoll_wait() error");
			break;
		}

		for(i=0; i<event_cnt; i++)
		{//当serv_sock发生变化时,创建用于和客户端通信的套接字
			if(ep_events[i].data.fd==serv_sock)
			{
				adr_sz=sizeof(clnt_adr);
				clnt_sock=
					accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
				event.events=EPOLLIN;
				event.data.fd=clnt_sock;
				//将与客户端通信的套接字放入epfd中进行监听
				epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
				printf("connected client: %d \n", clnt_sock);
			}
			else
			{  //对收到的信息进行读取
					str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
					if(str_len==0)    // close request!
					{  //删除epfd中监听的与客户端通信的套接字文件描述符
						epoll_ctl(
							epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
						close(ep_events[i].data.fd);  //关闭与客户端通信的套接字
						printf("closed client: %d \n", ep_events[i].data.fd);
					}
					else
					{  //发送接收到的信息
						write(ep_events[i].data.fd, buf, str_len);    // echo!
					}

			}
		}
	}
	close(serv_sock);
	close(epfd);  //关闭epoll实例的文件描述符
	return 0;
}

void error_handling(char *buf)
{
	fputs(buf, stderr);
	fputc('\n', stderr);
	exit(1);
}

条件触发和边缘触发

条件触发和边缘触发的区别在于发生事件的时间点

   条件触发方式中,只要输入缓冲有数据就会一直通知该事件。

  例如,服务器端输入缓冲收到50字节数据时,服务器端操作系统将通知该事件(注册到发生变化的文件描述符)。但服务器端读取20字节后还剩下30字节的情况下,仍会注册事件。也就是说,条件触发方式中,只要输入缓冲中还剩有数据,就将以事件方式再次注册。

  边缘触发中输入缓冲收到数据时仅注册1次该事件,即使输入缓冲还留有数据,也不会再进行注册。

条件触发的事件特性

  epoll默认以条件触发方式工作;select模型是以条件触发方式工作的。

边缘触发的服务器端实现中必知的两点

①通过erron变量验证错误原因

②为了完成非阻塞I/O,更改套接字特性

  Linux套接字相关函数一般通过返回-1通知发生了错误。为了在发生错误时提供额外的信息,Linux声明了如下全局变量:int errno。

  为了访问该变量,需要引入error.h头文件,因为此头文件中有上述变量的extern声明。另外,每种函数发生错误时,保存到erron变量中的值都不同。read函数发现输入缓冲中没有数据可读时返回-1,同时在erron中保存EAGAIN常量。

Linux提供更改或读取文件属性的方法:

 

实现边缘触发的回声服务器端

网上的理论:

理论上epoll的LT模式能够支持阻塞模式的socket,在阻塞模式下,当有数据到达,epoll_wait返回EPOLLIN事件,此时的处理中调用read读取数据,请注意,第一次调用read,可以保证socket里面有数据(EPOLLIN事件说明有数据),read不会阻塞。第二次调用,socket里面有没有数据是不确定的,要是贸然调用,read可能就阻塞了,因此不能进行第二次调用,必须等待epoll_wait再次返回EPOLLIN才能再次read。因此阻塞socket使用就必须read/write一次就转到epoll_wait,这对于网络流量较大的应用效率是相当低的,而且一不小心就会阻塞在某个socket上,因此多路复用+阻塞式socket几乎不出现在实际应用中

  在边缘触发方式中,接收数据时仅注册1次该事件。一旦发生输入相关事件,就应该读取输入缓冲中的全部数据,因此需要验证输入缓冲是否为空。read函数返回-1,变量errno中的值为EAGAIN时,说明没有数据可读。边缘触发方式下,以阻塞方式工作的read&write函数有可能引起服务器端长时间停顿。因此,边缘触发方式中一定要采用非阻塞read&write函数。


 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值