TCP/IP网络编程学习(13):epoll

再看select代码

while(1){
		cpy_reads=reads;
		timeout.tv_sec=5;
		timeout.tv_usec=5000;
		if((fd_num=select(fd_max+1, &cpy_reads, 0, 0, &timeout))==-1)
			break;
		
		if(fd_num==0)
			continue;

		for(i=0; i<fd_max+1; i++)
		{
			if(FD_ISSET(i, &cpy_reads))
			{
				if(i==serv_sock)     // connection request!
				{
					adr_sz=sizeof(clnt_adr);
					clnt_sock=
						accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
					FD_SET(clnt_sock, &reads);
					if(fd_max<clnt_sock)
						fd_max=clnt_sock;
					printf("connected client: %d \n", clnt_sock);
				}
				else    // read message!
				{
					str_len=read(i, buf, BUF_SIZE);
					if(str_len==0)    // close request!
					{
						FD_CLR(i, &reads);
						close(i);
						printf("closed client: %d \n", i);
					}
					else{
						write(i, buf, str_len);    // echo!
					}
				}
			}
		}
	}
  1. select函数采用轮询机制,调用select函数后需要遍历所以文件描述符以找到发生事件的文件描述符。
  2. 每次调用select函数时都需要向该函数传递监视对象信息 。

在调用select函数后并不是把发生变化的文件描述符集中到一起。而是通过观察作为监视对象的fd_set变量的变化,找出发生变化的文件描述符。而且,作为监视对象的fd_set变量会发生改变。调用select函数前应复制并保存原有信息并在每次调用select函数时传递新的监视对象信息 。

最大缺点:每次调用 select函数时向操作系统传递监视对象信息 。发生数据从用户态到内核态的拷贝。

epoll

仅向操作系统传递 1 次监视对象监视范围或内容发生变化时只通知发生变化的事项 。
无需编写以监视状态变化为目的的针对所有文件描述符的循环语句 。
调用对应于seleot函数的epoll_wait函数时无需每次传递监视对象信息 。

epoll_create: 创建保存epoll文件描述符的空间 。
epoll_ctl: 向空间注册并注销文件描述符。
epoll_wait: 与 select函数类似等待文件描述符发生变化 。

epoll由操作系统保存监视对象文件描述符,因此需要向操作系统请求创建保存文件描述符的空间,此时使用的函数就是epoll_create。此外,为了添加和删除监视对象文件描述符,select方式中需要FD_SET FD_CLR函数。 但在epoll方式中,通过epoll_ctl函数请求操作系统完成。最后,select方式下调用 select函数等待文件描述符的变化,而epoll中调用 epoll_wait函数 。 还有,select方式中通过fd_set变量查看监视对象的状态变化(事件发生与否),而epoll方式中通过如下结构体epoll_event将发生变化的 (发生事件的)文件描述符单独集中到一起。

struct epoll_event{
    __uint32_t event;
    epoll_data_t data; 
};

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

声明足够大的epoll_even结构体数组后,传递给epoll_wait函数时,发生变化的文件描述符信息将被填入该数组 。 因此,无需像select函数那样针对所有文件描述符进行循环 。

int epoll_create(int size);
// 成功时返回epoll文件描述描述符,失败返回-1
// epoll实例的大小
//创建的资源与套接字相同,也由操作系统管理。

//生成epoll例程后,应在其内部注册监视对象文件描述符,

int epoll_ctl(int epfd, int op, int fd, struct epol1_event * event);
// epfd epoll历程的文件描述符
// op 指定对象的添加、删除或更改操作
// fd 需要注册的监视对象文件描述符。
// event 监视对象的事件类型。
// op的选项
// EPOLL_CTL_ADD: 将文件描述符注册到 epoll例程 。
// EPOLL_CTL_DEL: 从epoll例程中删除文件描述符 。
// EPOLL_CTL_MOD: 更改注册的文件描述符的关注事件发生情况 。

int epoll_wait(int epfd, struct epoll_event * events, ïnt maxevents, int timeout);
// events 保存发生事件的文件描述符集合的结构体地址值 。所指缓冲需要动态分配 。
// 调用函数后,返回发生事件的文件描述符数,同时在第二个参数指向的缓冲内保存发生事件
// 的文件描述符集合。 因此,无需像select那样插入针对所有文件描述符的循环。

基于epoll的回声服务端

	epfd=epoll_create(EPOLL_SIZE);//注册epoll历程
	ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);

	event.events=EPOLLIN;
	event.data.fd=serv_sock;	
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);//向epoll历程中注册服务端套接字

	while(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++)//处理发生的事件
		{
			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;
				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!
					{
						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!
					}
	
			}
		}
	}

条件触发和边缘触发

水平触发(Lever trigger, LT),边缘触发(Edge Trigger, ET)。epoll默认是水平触发。

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

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

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

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

通过errno变量验证错误原因
为了完成非阻塞( Non-blocking ) I/O , 更改套接字特性 。

Linux 的 套接字相关函数一般通过返回 -1通知发生了错误 。 虽然知道发生了错误,但仅凭这些内容无法得知产生错误的原因 。 因此,为了在发生错误时提供额外的信息, Linux声明了如下全局变量 :

int errno

另外每种函数发生错误时,保存到 errno变量中的值都不同 ,没必要记住所有可能的值。

read函数发现输入缓冲中没有数据可读时返回-1,同时在 errno中保存EAGAIN常量 。
Lin以提供更改或读取文件属性的如下方法

int fcntl(int fd, int cmd, ...)
// 成功时返回 cmd 参数相关值,失败时返回-1

从上述声明中可以看到, fcntl具有可变参数的形式 。 如果向第二个参数传递F_GETFL ,可以获得第一个参数所指的文件描述符属性 ( int型 )。 反之,如果传递F_SETFL ,可以更改文件描述符属性 。 若希望将文件( 套接字)改为非阻塞模式,需要如下2条语句 。

int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);

通过第一条语句获取之前设置的属性信息,通过第二条语句在此基础上添加非阻塞O_NONBLOCK标志 。调用 read& write函数时,无论是否存在数据,都会形成非阻塞文件 (套接字 )。

首先说明为何需要通过errno确认错误原因 。"边缘触发方式中 ,接收数据时仅注册 1 次该事件 。 "就因为这种特点,一旦发生输入相关事件,就应该读取输入缓冲中的全部数据。因此需要验证输入缓冲是否为空 。"read函数返回-1, 变量erron 中的值 EAGAIN 时,说明没有数据可读。既然如此,为何还需要将套接字变成非阻塞模式?边缘触发方式下,以阻塞方式工作的read& write函数有可能引起服务器端的长时间停顿 。 因此 边缘触发方式中一定要采用非阻塞read&write 函数。

while(1)
	{
		event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
		if(event_cnt==-1)
		{
			puts("epoll_wait() error");
			break;
		}

		puts("return epoll_wait");
		for(i=0; i<event_cnt; i++)
		{
			if(ep_events[i].data.fd==serv_sock)
			{
				adr_sz=sizeof(clnt_adr);
				clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
				setnonblockingmode(clnt_sock);
				event.events=EPOLLIN|EPOLLET;
				event.data.fd=clnt_sock;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
				printf("connected client: %d \n", clnt_sock);
			}
			else
			{
					while(1)//边缘触发,循环读取所有数据
					{
						str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
						if(str_len==0)    // close request!
						{
							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);
							break;
						}
						else if(str_len<0)//返回-1说明没有数据
						{
							if(errno==EAGAIN)
								break;
						}
						else
						{
							write(ep_events[i].data.fd, buf, str_len);    // echo!
						}
				}
			}
		}
	}

边缘触发方式下可以做到如下这点 :"可以分离接收数据和处理数据的时间点!"即使输入缓冲收到数据(注册相应事件),服务器端也能决定读取和处理这些数据的时间点,这样就给服务器端的实现带来巨大的灵活性。 在水平触发条件下有数据就会产生时间,当客户端连接时数很大时,服务器难以承受。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值