IO多路复用三剑客之 epoll学习记录及简单应用

epoll 全称 eventpoll,是 linux 内核实现 IO 多路转接 / 复用(IO multiplexing)的一个实现

IO 多路转接的意思是在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。
epoll 是 select 和 poll 的升级版,相较于这两个前辈,epoll 改进了工作方式,因此它更加高效。

对于待检测集合select和poll是基于线性方式处理的,epoll是基于红黑树来管理待检测集合的。
select和poll每次都会线性扫描整个待检测集合,集合越大速度越慢,epoll使用的是回调机制,效率高,处理效率也不会随着检测集合的变大而下降
select和poll工作过程中存在内核/用户空间数据的频繁拷贝问题,在epoll中内核和用户区使用的是共享内存(基于mmap内存映射区实现),省去了不必要的内存拷贝。
我们需要对select和poll返回的集合进行判断才能知道哪些文件描述符是就绪的,
但是通过epoll可以直接得到已就绪的文件描述符集合,无需再次检测
使用 epoll 没有最大文件描述符的限制,仅受系统中进程能打开的最大文件数目限制
当多路复用的文件数量庞大、IO 流量频繁的时候,一般不太适合使用 select () 和 poll (),
这种情况下 select () 和 poll () 表现较差,推荐使用 epoll ()。

epoll操作函数
#include <sys/epoll.h>
// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);
// 管理红黑树上的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

select/poll 低效的原因之一是将== “添加 / 维护待检测任务”== 和== “阻塞进程 / 线程” ==两个步骤合二为一。
每次调用 select 都需要这两步操作,然而大多数应用场景中,需要监视的文件描述符个数相对固定,并不需要每次都修改。

epoll 将这两个操作分开,先用 epoll_ctl() 维护等待队列,再调用 epoll_wait() 阻塞进程(解耦)。

通过下图的对比显而易见,epoll 的效率得到了提升。
在这里插入图片描述

epoll_create()

/*
 * @description		: epoll_create()创建一个红黑树模型的实例,用于管理待检测的文件描述符的集合。
 * @param - size	: 在 Linux 内核 2.6.8 版本以后,这个参数是被忽略的,只需要指定一个大于 0 的数值就可以了。
 * @return 			: 返回一个有效的文件描述符,通过这个文件描述符就可以访问创建的 epoll 实例了; 失败则返回-1;
 */	
int epoll_create(int size);

epoll_ctl()

// 联合体, 多个变量共用同一块内存        
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 */
};

/*
 * @description		: epoll_ctl()管理红黑树实例上的节点,可以进行添加、删除、修改操作。
 * @param - epfd	: 填写epoll_create () 函数的返回值,通过这个参数找到 epoll 实例
 * @param - op		: 指定该函数执行什么操作,添加:EPOLL_CTL_ADD;修改:EPOLL_CTL_MOD;删除:EPOLL_CTL_DEL;
 * @param - fd		: 指定文件描述符,即要添加 / 修改 / 删除的文件描述符
 * @param - event	: 指定epoll事件指定检测这个文件描述符的什么事件,读:EPOLLIN;写:EPOLLOUT;异常:EPOLLERR;
 * @return 			: 成功,返回 0; 失败则返回-1;
 */	
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctl()

/*
 * @description			: epoll_wait()管理红黑树实例上的节点,可以进行添加、删除、修改操作。
 * @param - epfd		: 填写epoll_create () 函数的返回值,通过这个参数找到 epoll 实例
 * @param - events	 	: 传出参数,这是一个结构体数组的地址,里边存储了已就绪的文件描述符的信息
 * @param - maxevents 	: 修饰第二个参数,结构体数组的容量(元素个数)
 * @param - timeout		: 如果检测的 epoll 实例中没有已就绪的文件描述符,该函数阻塞的时长,单位ms毫秒。
 * 				timeout	等于0,表示函数不阻塞,不管 epoll 实例中有没有就绪的文件描述符,函数被调用后都直接返回
 *				timeout	大于0,表示如果 epoll 实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数ms再返回
 *				timeout	等于-1,表示函数一直阻塞,直到epoll 实例中有已就绪的文件描述符之后才解除阻塞
 * 							
 * @return 				: 成功,返回值等于0时,表示函数是阻塞被强制解除了,没有检测到满足条件的文件描述符
 * 						  成功,返回值大于0时,表示检测到的已就绪的文件描述符的总个数; 
 * 						  失败,返回-1;
 */	
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll实现单线程贪吃蛇

epoll_snake.c

#include <stdio.h>
#include <string.h>
#include <curses.h>
#include <stdlib.h>

#include <unistd.h>
#include <sys/epoll.h>

 // 创建epoll实例,通过一棵红黑树管理待检测集合
//int epoll_create(int size);
// 管理红黑树上的文件描述符(添加、修改、删除)
//int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 检测epoll树中是否有就绪的文件描述符
//int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//int poll(struct poll_fd *fds, nfds_t  nfds, int timeout)

#define UP    1
#define DOWN   -1
#define LEFT  2
#define RIGHT  -2

#define KEYCODE_R 0x43
#define KEYCODE_L 0x44
#define KEYCODE_U 0x41
#define KEYCODE_D 0x42

//初始位置
#define LINE0 1 
#define LIST0 1

struct Snake
{
	int line;
	int list;
	struct Snake *next;
};

struct Snake food ;
struct Snake *head = NULL;
struct Snake *tail = NULL;
int dir;

void initNcurses()
{		
	initscr();
	keypad(stdscr,1);
	noecho();
}

int hasSnakeNode(int i,int j)
{
	struct Snake *p;
	p = head;
	while(p != NULL){
		if(p->line == i&& p->list == j){
			return 1;
		}	
		p = p ->next;
	}
	return 0;
}

int hasFoodNode(int i,int j)
{
	if(food.line == i&& food.list == j){
		return 1;
	}	
	return 0;
}

void  gameMap()
{
	int line;
	int list;
	move(0,0);	
	for(line=0;line<20;line++){
		if(line==0){
			for(list = 0; list <20;list++){
				printw("--");
			}
			printw("\n");
		}
		if(line>=0 && line<=19){
			for(list=0;list<=20;list++){
				if(list ==0 || list==20){
					printw("|");
				}else if(hasSnakeNode(line,list)){
					printw("[]");
				}else if(hasFoodNode(line,list)){
					printw("##");
				}
				else{
					printw("  ");
				}
			}
			printw("\n");

		}
		if(line == 19){
			for(list=0;list<20;list++){
				printw("--");
			}
			printw("\n");
			printw("                            ~By HaHaHa!\n");

		}

	}

}

void addNode()
{
	struct Snake *new = (struct Snake *)malloc(sizeof(struct Snake));
	new->line = tail->line;
	new->list = tail->list+1;
	new->next = NULL;
	switch(dir){
		case UP:
			new->line = tail->line-1;
			new->list = tail->list;
			break;

		case DOWN:
			new->line = tail->line+1;
			new->list = tail->list;
			break;

		case LEFT:
			new->line = tail->line;
			new->list = tail->list-1;
			break;
		case RIGHT:
			new->line = tail->line;
			new->list = tail->list+1;
			break;
	}
	tail->next= new;
	tail = new;
}

void initFood()
{
	int x = rand()%20;
	int y = rand()%20;

	food.line = x;
	food.list = y;	
}

void initSnake()
{
	dir = RIGHT;		
	struct Snake *p;
	while(head != NULL){
		p = head;
		head = head->next;
		free(p);
	}

	head = (struct Snake *)malloc(sizeof(struct Snake));
	head->line = LINE0;
	head->list = LIST0;

	initFood();
	head->next = NULL;	
	tail = head;
	addNode();
	addNode();
}

void deleNode()
{
	struct Snake *p;
	p = head;
	head = head->next;
	free(p);
	p = NULL;
}

int ifSnakeDie()
{
	struct Snake *p;
	p = head;
	if(tail->line<0 || tail->list ==0 || tail->line==20 || tail->list==20){
		return 1;
	}
	while(p->next != NULL){
		if(p->line == tail->line && p->list == tail->list){
			return 1;
		}
		p =p->next;
	}	
	return 0;
}

void moveSnake()
{
	addNode();
	if(hasFoodNode(tail->line,tail->list)){
		initFood();
	} 
	else{
		deleNode();
	}
	if(ifSnakeDie()){
		initSnake();
	}
}

void funSnake()
{
	moveSnake();
	gameMap();
	refresh();
	usleep(108000);
}

void turn(int direction)
{
	if(abs(dir) != abs(direction))
	{
		dir = direction;
	}
}

void sys_err(const char *str)
{
    perror(str);
    exit(-1);
}

int main(void)
{
	int ret;
	char keycode;
	struct timeval tv;

	initNcurses(); 	//curses初始化
	initSnake(); 	
	gameMap();  
	// int epoll_create(int size);
	// 创建epoll实例,通过一棵红黑树管理待检测集合
	int epfd = epoll_create(1); //正数即可
	if(epfd == -1)
	{
        sys_err("epoll_create");
    }
	int kfd = STDIN_FILENO;
	struct epoll_event ev; 
	// 节点初始化
	ev.events = EPOLLIN; //指定检测 kfd读缓冲区是否有数据输入
	ev.data.fd = kfd;    //指定文件描述符
	
	// int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
	// 添加待检测节点到epoll实例中
	ret = epoll_ctl(epfd, EPOLL_CTL_ADD, kfd, &ev); //添加
	if(ret == -1)
    {
        sys_err("epoll_ctl");
    }
	 //evs传出参数,结构体数组的地址,存储了已就绪的文件描述符的信息
	struct epoll_event evs[1024]; //1024(进程可打开最大文件数)
    int size = sizeof(evs) / sizeof(struct epoll_event);

	while(1)
	{
		funSnake(); //贪吃蛇前进
		// 调用一次, 检测一次
		// int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
        ret = epoll_wait(epfd, evs, size, 50); //50 阻塞等待毫秒数
		if(ret > 0)
		{
			for(int i=0; i<ret; i++)
			{
				// 取出当前的文件描述符
				int curfd = evs[i].data.fd;
				// 判断
				if(curfd == kfd)
				{
					if(read(kfd, &keycode, 1) < 0)
					{
						sys_err("read");
					}
					//printf("%c\n",keycode);
					switch(keycode)
					{
						case KEYCODE_L:
							turn(LEFT);
							break;
						case KEYCODE_R:
							turn(RIGHT);
							break;
						case KEYCODE_U:
							turn(UP);
							break;
						case KEYCODE_D:
							turn(DOWN);
							break;
					}
				}

			}	
		}

	}
	getch();
	endwin();
	return 0;
}

贪吃蛇实现了自由奔跑

在这里插入图片描述

了解了解:

epoll 的工作模式

水平模式(默认)

水平模式可以简称为 LT 模式,LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核通知使用者哪些文件描述符已经就绪,之后就可以对这些已就绪的文件描述符进行 IO 操作了。如果我们不作任何操作,内核还是会继续通知使用者。
水平模式的特点:

读事件:如果文件描述符对应的读缓冲区还有数据,读事件就会被触发,epoll_wait () 解除阻塞
当读事件被触发,epoll_wait () 解除阻塞,之后就可以接收数据了
如果接收数据的 buf 很小,不能全部将缓冲区数据读出,那么读事件会继续被触发,直到数据被全部读出,如果接收数据的内存相对较大,读数据的效率也会相对较高(减少了读数据的次数)
因为读数据是被动的,必须要通过读事件才能知道有数据到达了,因此对于读事件的检测是必须的
写事件:如果文件描述符对应的写缓冲区可写,写事件就会被触发,epoll_wait () 解除阻塞
当写事件被触发,epoll_wait () 解除阻塞,之后就可以将数据写入到写缓冲区了
写事件的触发发生在写数据之前而不是之后,被写入到写缓冲区中的数据是由内核自动发送出去的
如果写缓冲区没有被写满,写事件会一直被触发
因为写数据是主动的,并且写缓冲区一般情况下都是可写的(缓冲区不满),因此对于写事件的检测不是必须的
边沿模式

边沿模式可以简称为 ET 模式,ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当文件描述符从未就绪变为就绪时,内核会通过epoll通知使用者。然后它会假设使用者知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知(only once)。如果我们对这个文件描述符做 IO 操作,从而导致它再次变成未就绪,当这个未就绪的文件描述符再次变成就绪状态,内核会再次进行通知,并且还是只通知一次。ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。
边沿模式的特点:

读事件:当读缓冲区有新的数据进入,读事件被触发一次,没有新数据不会触发该事件
如果有新数据进入到读缓冲区,读事件被触发,epoll_wait () 解除阻塞
读事件被触发,可以通过调用 read ()/recv () 函数将缓冲区数据读出
如果数据没有被全部读走,并且没有新数据进入,读事件不会再次触发,只通知一次
如果数据被全部读走或者只读走一部分,此时有新数据进入,读事件被触发,并且只通知一次
写事件:当写缓冲区状态可写,写事件只会触发一次
如果写缓冲区被检测到可写,写事件被触发,epoll_wait () 解除阻塞
写事件被触发,就可以通过调用 write ()/send () 函数,将数据写入到写缓冲区中
写缓冲区从不满到被写满,期间写事件只会被触发一次
写缓冲区从满到不满,状态变为可写,写事件只会被触发一次

综上所述:epoll 的边沿模式下 epoll_wait () 检测到文件描述符有新事件才会通知,如果不是新的事件就不通知,通知的次数比水平模式少,效率比水平模式要高。

边沿模式设置

边沿模式不是默认的 epoll 模式,需要额外进行设置。epoll 设置边沿模式是非常简单的,epoll 管理的红黑树示例中每个节点都是 struct epoll_event 类型,只需要将 EPOLLET 添加到结构体的 events 成员中即可:

struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;	// 设置边沿模式
fcntl() 非阻塞处理:

默认的阻塞行为可使用 fcntl() 函数修改为非阻塞。

// 设置完成之后, 读写都变成了非阻塞模式
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;                                                        
fcntl(cfd, F_SETFL, flag);

在非阻塞模式下,循环地将读缓冲区数据读到本地内存中,当缓冲区数据被读完了,调用的 read()/recv() 函数还会继续从缓冲区中读数据,此时函数调用就失败了,返回 - 1。对应的全局变量 errno 值为 EAGAIN 或者 EWOULDBLOCK 如果打印错误信息会得到如下的信息:Resource temporarily unavailable

// 非阻塞模式下recv() / read()函数返回值 len == -1
int len = recv(curfd, buf, sizeof(buf), 0);
if(len == -1)
{
    if(errno == EAGAIN)
    {
        printf("数据读完了...\n");
    }
    else
    {
        perror("recv");
        exit(0);
    }
}

来源: 爱编程的大丙

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值