IO多路复用之epoll模型

IO多路复用之epoll模型

epoll简介

基于select和poll轮询方式的低效性,epoll为了解决这个不足,应运而生,epoll可以告诉服务器到底是哪些事件就绪了,epoll返回一个事件集合数组,告诉我们是前多少个事件就绪了,这样我们只需要遍历就绪事件集就可以了。就像我们学生向老师提问的案例一样,当有若干学生向老师举手时,老师把这些学生全部放到前三排的位置,然后告诉我们前三排的学生全部都是有问题的学生,我们只需回答前三排的学生的问题。

如果有10000个连接,但是9999个连接都发送数据给服务器了,select poll epoll 三者差不多
但是如果只有少数连接发送数据epoll的优势就体现出来了,而日常生活中这种情况很常见

epoll api

epoll_create

构建一颗红黑树,返回根节点的文件描述符
int epoll_create(int size)
size:告诉内核监听的数目

epoll_ctl

对红黑树的操作,增删改
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epfd:为epoll_create的句柄

op:表示动作,用3个宏来表示:
EPOLL_CTL_ADD(注册新的fd到epfd)EPOLL_CTL_MOD(修改已经注册的fd的监听事件)EPOLL_CTL_DEL(从epfd删除一个fd);

fd:当前操作的文件描述符

event:告诉内核需要监听的事件
struct epoll_event {
	__uint32_t events; /* Epoll 事件*/
	epoll_data_t data; /* 共用体,用户数据 */
};
typedef union epoll_data {
void *ptr;//有了这个扩展性大大增强,可以使用函数指针
int fd;//文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;

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

epoll_wait

int nready=int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
epfd:红黑树跟文件描述符
events:从内核得到事件的集合,遍历这个事件的前nready个元素即可
maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
timeout:是超时时间
-1:阻塞
0:立即返回,非阻塞
>0:指定微秒
返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1

epoll入门案例

案例的示意图如下
在这里插入图片描述

#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<errno.h>
#define MAXLINE 1024
#define SERV_PORT 9999
#define OPEN_MAX 2048

int main(){
	//listenfd:服务器建立socket返回的用户监听客户端连接的文件描述符 
	//connfd:客户端连接后返回的文件描述符,用户客户端服务器间通信使用
	int i,j,listenfd,connfd,sockfd;
	// read 返回的 
	int n;
	//nready就绪事件个数 ,efd是epoll内核epoll_create生成红黑树 的树根的文件描述符 
	ssize_t nready,efd,res;
	//buf 读写缓冲,clip,客户端ip 
	char buf[MAXLINE],clip[INET_ADDRSTRLEN];
	//客户端长度 
	socklen_t clilen;
	//客户端地址 服务器地址 
	struct sockaddr_in cliaddr,servaddr;
	
	struct epoll_event tep,ep[OPEN_MAX];
	int num=0;
	//socket 
	listenfd = socket(AF_INET,SOCK_STREAM,0);
	//服务器ip port 
	bzero(&servaddr,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(SERV_PORT);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	//绑定监听客户端连接 
	bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
	listen(listenfd,128);
	//构建一颗红黑树返回树根的文件描述符 
	efd = epoll_create(OPEN_MAX);
	
	//监听读时间 
	tep.events = EPOLLIN;//默认水平触发 
	//文件描述符 listenfd
	tep.data.fd = listenfd;
	//把 listenfd所在的 epoll_event加入红黑树 
	//int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
	res = epoll_ctl(efd,EPOLL_CTL_ADD,listenfd,&tep);
	//死循环 
	for(;;){
		//int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
		//events:用来从内核得到事件的集合,即内核会把就绪事件放入ep 
		//maxevents:告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
		nready = epoll_wait(efd,ep,OPEN_MAX,-1);//阻塞 
		//这个ep数组就是epoll返回的就绪事件集合, nready是就绪事件个数,我们服务器只需要遍历该集合的前 nready个事件即可全部处理所有客户端的请求 
		//这也是与select poll最大的不同 
		for(i=0;i<nready;i++){
			if(!(ep[i].events&EPOLLIN))
			continue;
			//建立连接事件 
			if(ep[i].data.fd==listenfd){
				clilen = sizeof(cliaddr);
				connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&clilen);
           		printf("received from %s at PORT %d\n",
                    inet_ntop(AF_INET, &cliaddr.sin_addr, clip, sizeof(clip)),
                    ntohs(cliaddr.sin_port));
                printf("cfd%d\tclient%d\n",connfd,++num);
                
                tep.events = EPOLLIN;
                tep.data.fd = connfd;
                //connfd加入 红黑树 
                res = epoll_ctl(efd,EPOLL_CTL_ADD,connfd,&tep);
			}else{
				//客户端发送数据 
				sockfd = ep[i].data.fd;
				n = read(sockfd,buf,MAXLINE);
				if(n==0){
					res = epoll_ctl(efd,EPOLL_CTL_DEL,sockfd,NULL);
					close(sockfd);
					printf("client[%d] closed connection\n", sockfd);
				}else if(n<0){
					printf("read err");
					res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
					close(sockfd);
				}else{
					printf("%s\n",buf);
					for (j = 0; j < n; j++)
						buf[j] = toupper(buf[j]);
					write(sockfd, buf, n);
				}
			}
		}
	}
	close(listenfd);
	close(efd);
	return 0;
}

客户端

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h> 
#define DEST_PORT 9999//目标地址端口号 
#define DEST_IP "127.32.255.2"/*目标地址IP,这里设为本机,不一定非得是127.0.0.1,只要127开头并且不是127.0.0.0和127.255.255.255即可*/ 
#define MAX_DATA 100//接收到的数据最大程度 
 
int main(){
	int sockfd;
	struct sockaddr_in dest_addr;
	char buf[MAX_DATA];
 
	sockfd=socket(AF_INET,SOCK_STREAM,0);
	
	dest_addr.sin_family=AF_INET;
 	dest_addr.sin_port=htons(DEST_PORT);
	dest_addr.sin_addr.s_addr=inet_addr(DEST_IP);
	bzero(&(dest_addr.sin_zero),8);
	connect(sockfd,(struct sockaddr*)&dest_addr,sizeof(struct sockaddr));

	printf("connect success");
	while(1){
		char send_buf[512] = "";
		scanf("%s",&send_buf);
		write(sockfd,send_buf,sizeof(send_buf));
		
		read(sockfd,send_buf,sizeof(send_buf));
    	printf("client receive:%s\n",send_buf);
	}

	return 0;
} 

centos执行
gcc -o server.out server.c
gcc -o client.out client.c
得到执行文件server.out,client.out
用xshell 对一个虚拟机开两个item窗口,一个执行./server.out ,另一个执行./client.out,可以开多个客户端通信

epoll的ET与LT

在这里插入图片描述如上图所示:这是一段电位波形图
高低电位,上升沿,下降沿如图所示
比如手机按键,不按的时候是高电位,按下去接地变成低电位,如果是按下按键触发按钮就是下降沿,如果是松开按键触发按钮就是上升沿。
边沿触发的意思就是在上升沿或者下降沿的瞬间触发事件
水平触发的意思就是在低电位的时候触发。例如长按不松开

EPOLL事件有两种模型:
Edge Triggered (ET) 边缘触发只有数据到来,才触发,不管缓存区中是否还有数据。
Level Triggered (LT) 水平触发只要有数据都会触发。

ET和LT例子1:

#include<stdio.h>
#include<stdlib.h>
#include<sys/epoll.h>
#include<errno.h>
#include<unistd.h>
#define MAXLINE 10

int main(){
	//efd是epoll根节点的文件描述符 
	int efd ,i;
	//管道两个文件的文件描述符 
	int pfd [2];
	//进程 
	pid_t pid;
	//缓冲区 
	char buf[MAXLINE] ,ch ='a';
	//管道 
	pipe(pfd);
	//fork进程 
	pid = fork();
	
	//子进程 
	if(pid==0){
		
		close(pfd[0]);
		while(1){
			for(i=0;i<MAXLINE/2;++i)
				buf[i] = ch;
			
			buf[i-1] = '\n';
			//aaaa\n 
			ch++;//b
			for(;i<MAXLINE;++i)
				buf[i] = ch;
			buf[i-1] = '\n';
			//aaaa\nbbbb\n 
			ch++;//c
			//写给父进程 
			write(pfd[1],buf,sizeof(buf));
			sleep(2);
		}
		close(pfd[1]);
	}else if(pid>0){//父进程 
		struct epoll_event event;
		struct epoll_event revent[MAXLINE];
		int res,len;
		close(pfd[1]);
		efd = epoll_create(MAXLINE);
		//边沿触发 ET 
		event.events = EPOLLIN | EPOLLET;
		//水平触发 LT 
		//event.events = EPOLLIN;
		event.data.fd = pfd[0];
		epoll_ctl(efd,EPOLL_CTL_ADD,pfd[0],&event);
		
		while(1){
			res = epoll_wait(efd,revent,MAXLINE,-1);
			//每次 读取缓冲区一半的内容 
			if(revent[0].data.fd == pfd[0]){
				len = read(pfd[0],buf,MAXLINE/2);
				write(STDOUT_FILENO,buf,len); 
			}
		} 
		close(pfd[0]);
		close(efd);
	}else{//fork error 
		perror("fork");
		exit(-1);
	}
	return 0;
}

在这里插入图片描述ET模式下输出:
aaaa
隔两秒
bbbb
隔两秒
cccc

LT模式输出:
aaaa
bbbb
隔两秒
cccc
dddd
隔两秒
eeee
ffff

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值