Linux中Epoll函数简单使用

对epoll不够了解可以先看看它的底层实现:The Implementation of epoll (1)

多路IO Multiplex IO

多路IO的作用:

1、阻塞 I/O 只能阻塞一个 I/O 操作,而 I/O 复用模型能够阻塞多个 I/O 操作,所以才叫做多路复用

2、采用epoll模型时创建了一个共享的内存空间,操作系统采用事件通知的方式,使一个进程能同时等待多个文件描述符

3、这样就可以同时监听多个网络连接 IO, 相对于多进程、多线程切换的开销问题,IO 多路复用可以极大的提升系统效率。
谈到多路IO,Linux下最常用的也就是三种IO模型: Select、Poll、Epoll,简单介绍一下epoll的使用。

epoll API

作为三大IO复用函数中比较常用的epoll, 它在实现和使用上与select poll有很大差异.首先, epoll使用一组函数来完成任务, 而不是单个函数. 其次, epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中, 从而无须向 select 和 poll 那样每次调用都要重复传入文件描述符集或事件集, 但 epoll 需要使用一个i额外的文件描述符, 来唯一标识内核中的这个事件表, 这个文件描述符由以下的 epoll_create 函数来创建.

epoll_create
EPOLL_CREATE(2)       Linux Programmer's Manual      EPOLL_CREATE(2)

NAME
       epoll_create, epoll_create1 - open an epoll file descriptor

SYNOPSIS
       #include <sys/epoll.h>

       int epoll_create(int size);
       
       DESCRIPTION
       epoll_create()  creates  an  epoll(7)  instance.  Since Linux
       2.6.8, the size argument is ignored, but must be greater than
       zero; see NOTES below.

       epoll_create() returns a file descriptor referring to the new
       epoll instance.  This file descriptor is  used  for  all  the
       subsequent  calls  to  the  epoll  interface.  When no longer
       required, the  file  descriptor  returned  by  epoll_create()
       should  be  closed by using close(2).  When all file descrip‐
       tors referring to an epoll instance  have  been  closed,  the
       kernel  destroys  the  instance  and  releases the associated
       resources for reuse.

翻译一下就是:用于创建一个 epoll 实例,size参数自从Linux 2.6.8以来已经被忽略,只需要传入大于0的数即可,函数返回一个文件描述符绑定这个新的epoll实例,当不再使用时需要用close函数关闭它。当所有与epoll实例相关的文件描述符都被关闭的时候,内核会销毁这个实例并且释放对应的资源以便再次使用。

epoll_ctl

epoll_ctl 函数用于操作内核事件表,Linux手册中是这么定义它的:

EPOLL_CTL(2)               Linux Programmer's Manual              EPOLL_CTL(2)

NAME
       epoll_ctl - control interface for an epoll descriptor

SYNOPSIS
       #include <sys/epoll.h>

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

用于操作epoll实例中事件的注册与销毁,以及监听事件的操作类型:

DESCRIPTION
       This  system  call performs control operations on the epoll(7) instance
       referred to by the file descriptor epfd.  It requests that  the  opera‐
       tion op be performed for the target file descriptor, fd.

第一个参数 epfd:传入需要操作的epoll实例。

第二个参数 op,有三个常用的宏:
EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL,对应完成了事件(fd)的注册、事件的修改、事件的注销(删除)。
第三个参数 fd 传入需要操作的文件描述符
第四个参数***epoll_event***是一个结构体,定义如下:

 The struct epoll_event is defined as:
           typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;

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

结构体定义了一个 表示事件类型的 uint32_t events,该参数有常用的几个宏:(只是在poll的宏前面加上了’E’)
EPOLLIN: 表示允许相关的文件描述符进行读操作。
EPOLLOUT: 表示允许相关的文件描述符进行写操作。
其余还有例如: EPOLLERR、EPOLLHUP、EPOLLET… 具体内容可以打开linux使用 man 2 epoll_ctl 查看。
这些宏都是位掩码,因此如果需要多种操作类型或者属性定义的话可以使用:
EPOLLIN | EPOLLOUT,进行位或。
结构体中另一个属性:
epoll_data_t data; epoll_data_t是一个联合,这里笔者只使用了fd这一属性。
深入了解epoll_data点这里

epoll_wait

这是epoll中最为主要的接口函数了,在一段超时时间内等待一组文件描述符上的事件,原型如下:

EPOLL_WAIT(2)                     Linux Programmer's Manual                    EPOLL_WAIT(2)

NAME
       epoll_wait - wait for an I/O event on an epoll file descriptor

SYNOPSIS
       #include <sys/epoll.h>

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

DESCRIPTION
       The epoll_wait() system call waits for events on the epoll(7) instance referred to by
       the file descriptor epfd.  The memory area pointed to  by  events  will  contain  the
       events  that  will  be  available  for  the  caller.  Up to maxevents are returned by
       epoll_wait().  The maxevents argument must be greater than zero.

  • struct epoll_event events
    参数events指示了调用者可用的事件类型,如果epoll实例监视的文件描述符发生了IO事件且我们关心的操作可实施时,内核会将这些文件描述符的信息写入events,因此这是一个传入传出参数。
  • int maxevents
    参数指定了events数组的大小,如果需要监听的事件比较多,那么可以使用大数组,监听数量不受限。
  • int timeout
    参数指定了epoll_wait()阻塞的时长,单位是毫秒(milisecond),以下三种情况会解除阻塞:
    Ⅰ. 一个文件描述符发生了事件( a file descriptor delivers an event)
    Ⅱ. 函数被信号处理器中断(the call is interrupted by a signal handler)
    Ⅲ. timeout爆发(the timeout expires)

epoll_wait 函数如果检测到事件, 就将所有就绪的事件从内核事件表(由 epfd 参数指定)中复制到它的第二个参数 events 指向的数组中, 这个数组只用于输出 epoll_wait 检测到的就绪事件, 而不像 select 和 poll 的数组参数那样既用于传入用户注册的事件, 又用于输出内核检测到的就绪事件, 这就极大地提高了应用程序索引就绪文件描述符的效率
下面的一段代码展示了这个区别:

int main(int argc, char* argv[]){
	/* 如何索引 poll 返回的就绪文件描述符 */
	int ret = poll( fds, MAX_EVENT_NUMBER, -1 );
	/* 必须遍历所有已注册文件描述符并找到其中的就绪事件 */
	for(int i = 0;i  < MAX_EVENT_NUMBER; ++i){
		if( fds[i].revents & POLLIN ){
			int sockfd = fds[i].fd;
			if(--ret){
				break;//这里用ret做了一点优化
			}
			/* 处理sockfd */
		}
	}

	/* 如何索引epoll返回的就绪文件描述符 */
	int ret = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
	/* 仅遍历就绪的ret个文件描述符 */
	for(int i = 0 ; i < ret; ++i){
		int sockfd = events[i].data.fd;
		/* sockfd肯定就绪,直接处理 */
	}
	return 0;
}
返回值
RETURN VALUE
       When  successful,  epoll_wait()  returns the number of file descriptors ready for the
       requested I/O, or zero if no file descriptor became ready during the requested  time‐
       out  milliseconds.   When  an  error occurs, epoll_wait() returns -1 and errno is set
       appropriately.

若成功调用,返回已经就绪的文件描述符数量,如果是时间爆发,那么返回0. 出错返回-1并且将errno设置成合适值。

epoll使用示例

笔者使用epoll实现了简单的多路IO,监听多个客户端的读事件,读取客户机发来的信息,将其显示在标准输出上,并写回客户端。
服务端代码

/**************************bin************************
******************************************************
******************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#define MAXFD 1025
int main(int argc, char* argv[]){
	//创建服务端套接字
	int serv_sock = socket(AF_INET, SOCK_STREAM, 0);
	struct sockaddr_in serv_addr, clnt_addr;
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(6666);
	serv_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	//绑定套接字
	bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
	//监听套接字
	listen(serv_sock, 10);
	socklen_t addrlen = sizeof(clnt_addr);
	int epfd = epoll_create(MAXFD);
	//将客户套接字挂载到epoll instance上
	struct epoll_event epollEvent;
	epollEvent.events = EPOLLIN;//申请注册读事件
	epollEvent.data.fd = serv_sock;
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &epollEvent);
	//传入参数,接收内核写入的被激活的事件
	struct epoll_event events[MAXFD];
	while(1){
		//timeout 传入 -1, 无限阻塞 infinitely
		int waitret = epoll_wait(epfd, events, MAXFD, -1);
		if(waitret == -1){//出错
			perror("epoll_wait error");
			exit(1);//线程退出
		}
		else{
			//对每个事件进行处理
			for(int i = 0; i < waitret; ++i){
				if(events[i].data.fd == serv_sock){
					//与我们在外面监听的是同一个套接字
					int cfd = accept(serv_sock,(struct sockaddr*)&clnt_addr, &addrlen);
					char dst[64];
					printf("client is OK, IP:%s, PORT:%d\n", inet_ntop(AF_INET, &clnt_addr.sin_addr.s_addr, dst, sizeof(dst)),ntohs(clnt_addr.sin_port));
					epollEvent.data.fd = cfd;	
					epollEvent.events = EPOLLIN;
					epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epollEvent);
						
					}
				else{	
						//保存文件描述符
						int fd = events[i].data.fd;
						char buf[1024];
						//从fd中读取三个 byte
						int rr = read(fd, buf, 3);
						if(rr < 0){		
							perror("read error");
							exit(1);
						}
						else if(rr == 0){
							printf("\nclient is out\n");										close(fd);
							epoll_ctl(epfd, EPOLL_CTL_ADD, fd, NULL);
						continue;
						}
						else {//读到数据
							buf[rr] = '\n';
							write(1, buf, rr);
							write(fd, buf, rr);//写回
						}
					}
				}
			}
		}
		
	return 0;
}

/**************************bin************************
******************************************************
******************************************************

客户端代码

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


int main(int argc, char* argv[]){
	int clntSock = socket(PF_INET, SOCK_STREAM, 0);
	struct sockaddr_in addr;
	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_port = htons(6666);
	addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	connect(clntSock, (struct sockaddr*)&addr, sizeof(addr));
	//写入数据
	char buf[1024];
	while(1){
	memset(buf, '\0', 1024);
	printf("请输入:\n");
	scanf("%s", buf);
	if(buf[0] == buf[1] && buf[0] == 'g')
		break;
	send(clntSock, buf, sizeof(buf), 0);
	char newBuf[1024];
	int rr = recv(clntSock,newBuf, 1024, 0);
	newBuf[rr] = '\0';	
	printf("服务器回射信息:%s\n\n", newBuf);
	}
	close(clntSock);
	return 0;
}

/**************************bin************************
******************************************************
******************************************************/
运行

在这里插入图片描述

参考 --------《Linux高性能服务器编程》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nepu_bin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值