通俗易懂说多路复用(2)epoll


多路复用系列:
通俗易懂说多路复用(1)select
https://blog.csdn.net/lqy971966/article/details/89173936
通俗易懂说多路复用(2)epoll
https://blog.csdn.net/lqy971966/article/details/89217648
通俗易懂说多路复用(3)eventfd 事件通知
https://blog.csdn.net/lqy971966/article/details/104751751
通俗易懂说多路复用(4)fcntl
https://blog.csdn.net/lqy971966/article/details/105390106

1. poll

  1. poll和select本质上没有区别;
    最大的区别:由于poll是基于链表来存储描述符的,所以poll有最大连接数的限制。

  2. select 链接数的限制:
    正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。

2. epoll

2.1 出现缘由:解决select和poll的轮询方式效率低问题

epoll是为了解决select和poll的轮询方式效率低问题

2.2 定义:epoll是为了处理大量句柄而做改进的poll

man对epoll的定义:为了处理大量句柄而做改进的poll。
可以理解为:event poll

2.3 通俗理解:

假设你在家一边玩游戏过程中,水壶正在烧水,洗衣机同时在洗衣服……。不久,游戏过程中,水壶响了,你就去倒水;洗衣机洗好了,你就去晾衣服……你通过听它们的响声“滴滴滴”来判断事情有没有完成,然后进行下一步操作。
epoll就相当于:任务完成通过“滴滴滴”通知你,然后你去挨个处理不同的任务。

2.4 优点:无数量限制/无需拷贝/事件驱动避免轮询/抛弃了bitmap

  1. 对fd数量没有限制(当然这个在poll也被解决了)
  2. 抛弃了bitmap数组实现了新的结构来存储多种事件类型
  3. 无需重复拷贝fd 随用随加 随弃随删
  4. 采用事件驱动避免轮询查看可读写事件

2.5 复杂度:O(1)

O(1)

2.6 相关接口:

2.6.1 接口之 epoll_create:

原型:

 int epoll_create(int size)

功能: 创建epoll的对象
注意: 一般写成:int fd = epoll_create(1) ,因为在epoll早期的实现中,对于监控文件描述符的组织是hash表,而现在是红黑树,所以现在参数size已经没有意义了,一般写1就可以。

2.6.2 接口之 epoll_ctl:

原型:

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

功能: 增加/修改/删除描述符,监控事件
参数说明:
epfd:epoll_create()创建的epoll对象/描述符
fd:要操作的描述符
op:对该描述符的操作(增/修/删)
op的操作类型有:

EPOLL_CTL_ADD:往时间表中注册/增加事件
EPOLL_CTL_MOD:修改fd上的注册事件
EPOLL_CTL_DEL:删除fd上的注册事件

event:指定事件,告诉内核需要监听什么事件。
events可以是以下几个宏的集合:

EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,
          这是相对于水平触发(Level Triggered)来说的。

它是epoll_event 结构体指针类型

struct epoll_event
{
	__unit32_t events; //epoll事件
	epoll_data_t data; //存储用户数据
}
typedef union epoll_data
{
	void *ptr; //指定与fd相关的用户数据
	int fd; //指定事件所属于的目标文件描述符
	uint32_t u32; 
	uint64_t u64;
}epoll_data_t;

2.6.3 接口之 epoll_wait:

原型:

int epoll_wait(int epfd, struct epoll_event *event, int maxevents, int timeout)

功能: 等待指定文件描述符就绪,收集在epoll监控的事件中已经发送的事件。
返回: 成功时返回就绪的文件描述符的个数,失败返回-1
参数说明:
epfd::等待的文件描述符
events:检测到事件
timeout:指定epoll的超时时间,单位是毫秒
当timeout = -1时,将永远阻塞,直到某个时间发生;
当timeout = 0时,调用后立即返回;

timeout:epoll超时时间,单位毫秒
为-1表示阻塞,
为0表示非阻塞,其他值表示超时时间。这个会导致cpu过高
这里一定要注意,程序涉猎多线程和多进程时,这个timeout的设置初学时也容易出错。
一般设置为1,毫秒

2.6.4 举个现实中的例子,来理解上述三个接口

epoll_create场景
大学开学第一周,你作为班长需要帮全班同学领取相关物品,你在学生处告诉工作人员,我是xx学院xx专业xx班的班长,这时工作人员确定你的身份并且给了你凭证,后面办的事情都需要用到(也就是调用epoll_create向内核申请了epfd结构,内核返回了epfd句柄给你使用);

epoll_ctl场景
你拿着凭证在办事大厅开始办事,分拣办公室工作人员说班长你把所有需要办理事情的同学的学生册和需要办理的事情都记录下来吧,于是班长开始在每个学生手册单独写对应需要办的事情:李明需要开实验室权限、孙大熊需要办游泳卡…就这样班长一股脑写完并交给了工作人员(也就是告诉内核哪些fd需要做哪些操作);

epoll_wait场景
你拿着凭证在领取办公室门前等着,这时候广播喊xx班长你们班孙大熊的游泳卡办好了速来领取、李明实验室权限卡办好了速来取…还有同学的事情没办好,所以班长只能继续(也就是调用epoll_wait等待内核反馈的可读写事件发生并处理);

2.7 epoll工作原理:

epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

这个参考原文:https://blog.csdn.net/xiajun07061225/article/details/9250579

2.8 ET 模式和LT模式:

默认采用LT模式,LT支持阻塞和非阻塞套,ET模式只支持非阻塞套接字,其效率要高于LT模式,并且LT模式更加安全。

2.8.1 ET 模式(Edge Triggered 边缘触发):只通知一次

  1. ET是一种高速处理模式,只有状态改变时才进行通知;
  2. 所以要求服务端/或程序必须一次把数据读完;
  3. 如果不处理可能就丢了;

当epoll_wait()检测到fd上有事件发生并将此事件通知应用程序后,应用程序必须立即处理,因为后续的epoll_wait()调用将不再通知这个事件。
所以,ET模式要求
特点: 减少系统调用来提升效率。
缺点:对代码要求比较高

实现:
event.events = EPOLLIN | EPOLLET;

2.8.2 LT 模式(Level Triggered 水平触发):一直通知

  1. 该模式是 epoll 的缺省模式,支持阻塞和非阻塞的socket;
  2. 内核会告诉程序一个文件描述符是否就绪,如果没处理,内核会一直通知;
    当epoll_wait()检测到fd上有事件发生并将此事件通知应用程序后,应用程序不必立即处理,因为后续的epoll_wait()调用将还会再通知这个事件,直到此事件被处理。
  3. 缺点:如果未处理,会一直上报 epoll_wait 事件,导致cpu高
  4. event.events = EPOLLIN; // LT是默认模式

2.8.3 ET 模式和LT模式总结(推荐LT模式,水平出发,一直通知)

LT模式,如果来了一个消息,你没处理或者没处理完,就一直通知你,直到处理完为止;
event.events = EPOLLIN; // LT是默认模式
ET模式,如果来了一个消息,只通知你一次,不处理就丢了
event.events = EPOLLIN | EPOLLET;

2.9 (VIP!!!)FAQ:

Q:我用epoll监控了一个队列,队列有多个消息写入时,epoll会相应报多次么?
A:epoll事件对于同一个事件源,未得到调度时的累加只表现为一个事件

Q:epoll监控的队列里,一次处理下没有将消息全部读完,还会上报事件么?
A:事件注册时尽量不要选择EPOLLET方式,
	EPOLLET为边沿触发方式,有可能导致事件不再上报的情况;
	默认epoll为EPOLLLT方式,即Level Triggered电平触发方式,这样只要有事件,就会持续上报,是推荐的方式。

Q:epoll监控多个事件源,如果一次处理下没有将消息全部处理完,再次上报时,会还在其他事件源之前上报么?
A: 假设在阻塞点时,已有6个事件源处于有消息到来,即处于ready状态,假定此时读取的events数组为4:
		A1 A2 A3 A4 A5 A6
	那么会将ready队列的前四个事件源摘下,放到events数组里返回给epoll循环读取处理。
	假设事件源1、4一次处理就全部读清,而事件源2、3没有一次处理完,那么epoll的ready队列现在状态会是:
		A5 A6 A2 A3
	可以看到,之前读取过的事件源,是依次放到队列尾的。

2.10 代码示例:

#include<stdio.h>
#include<unistd.h>
#include<sys/epoll.h>
int main(void)
{
	int epfd,nfds;
	struct epoll_event ev,events[5];
	epfd = epoll_create(1);
	ev.data.fd = STDIN_FILENO;
	ev.events = EPOLLIN;
	epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);
	for(;;)
	{
		nfds = epoll_wait(epfd,event,5,-1);
		for(int iLoop = 0;iLoop < nfds;iLoop++)
		{
			if(events[i].data.fd == STDIN_FILENO)
			{
				char buf[1024] = {0};
				read(STDIN_FILENO, buf, sizeof(buf));
				printf("epoll ok\n");
			}
		}
		return 0;
	}

3. 参考

[1]: 我读过的最好的epoll讲解 -知乎
[2]:https://www.cnblogs.com/lojunren/p/3856290.html
[3]:https://blog.csdn.net/xiajun07061225/article/details/9250579
https://zhuanlan.zhihu.com/p/63179839

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值