参考(原文简直超赞): https://zhidao.baidu.com/question/687563051895364284.html
下面是我结合原文写的,为了便于自己理解:
关于阻塞和非阻塞的理解可以看这个:http://www.cnblogs.com/xcywt/p/8146123.html
1. 举例子说明
假设你在读大学,有个朋友 F 来找你,你住在 A 栋。但是不知道具体是哪个房间。于是你们约好在 A 栋门口见面。
如果用阻塞 IO 模型来处理这个问题,你就相当于一直在 A 栋门口等着,这个时候你不能做别的事情,效率比较低,如果 F 一直不来你就得一直在那等着。
接着来看用非阻塞模型来处理这个问题,主要有两种 select/poll(这两个可以看成一种)和 epoll:
select 大妈做的事情是这样:当朋友 F 到了楼下时,她带着 F 一个个房间了轮询的去找你。
epoll 大妈就比较高级了:大妈拿本子记录下你的房间号,当朋友 F 来的时候告诉 F 你的房间号。这样就不用整栋楼去跑了。
在大并发服务器中,轮询 IO 是一件比较费时的操作,就跟 select 大妈一样。
epoll 大妈多用了一个本子,就有点用空间去换取时间的意思。
2.select/poll 为什么慢:
1)select/poll 是遍历所有添加进 fd_set 的 fd。并且需要将所有用户态的 fd 拷贝到内核态。数量巨大时这个效率比较慢
2)并且返回之后,还要轮询将所有集合查询一次
3)内核空间的数据需要拷贝到用户空间
3.epoll 的实现原理:
具体使用方法可以参考:http://www.cnblogs.com/xcywt/p/8146094.html
先说几个函数的作用
int epoll_create(int size); // 创建一个 epoll 对象,size 是内核保证能够正确处理的最大句柄数。
int epoll_create1(int flags);// 上面的加强版本,参数只能是 EPOLL_CLOEXEC
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);// 在给定时间内,监控的所有句柄中有时间发生就返回
下面我们来看具体做了什么:
epoll 在内核初始化的时候向内核注册了一个文件系统,用于存储上述被监控的 socket,同时还会开辟出 epoll 自己的内核高速 cache 区,用于安置需要监控的 fd。这些 fd 以红黑树的形式保存在内核 cache 里,以支持快速的查找、插入、删除。这个内核高速 cache 区,就是建立连续的物理内存页,然后在之上建立 slab 层,简单的说就是物理上分配好你想要的大小的内存对象,每次使用时都是使用空闲的已分配好的对象。
每次调用 epoll_create 时,会在这个虚拟的 epoll 文件系统里创建一个 file 节点,在内核 cache 中建立个红黑树来存储通过 epoll_ctl 添加进来的 fd。这些 fd 其实已经在内核态了,当你再次调用 epoll_wait 时,不需要再拷贝进内核态(select 需要再全部拷贝到内核态)。
同时还会建立一个 list 链表,用来存储已经就绪的事件。被 epoll_wait 调用时,就去看这个 list 链表是不是为空,若不为空就返回,为空就等待指定的事件再返回。
list 链表是如何维护的呢:当我们执行 epoll_ctl 时,会把对应 fd 放到红黑树中,还会给内核终端处理程序注册一个回调函数。如果这个句柄的中断到了,就把它放在 list 链表中去。
总结一下:一棵红黑树和一个 list 链表就解决大并发的问题。epoll_create 时创建红黑树和就绪链表,epoll_ctl 时添加到红黑树中(若存在则不添加)并向内核注册回调函数。epoll_wait 时返回 list 就绪链表里面的数据就可以了。
4.epoll 的两个工作模式:
LT:只要一个句柄上的事件一次没有处理完,接着调用 epoll_wait 时仍然会返回这个句柄。
ET:尽在空闲状态 -> 就绪状态返回一次。
这件事是怎么做到的呢:当有 fd'发生事件时,就放到 list 就绪链表中去了。然后 epoll_wait 返回,再然后清空准备 list 就绪链表。
最后如果是 LT 模式,并且仍有未处理的事件,就把这个 fd 重新放回到 list 就绪链表中。
如果是 ET,就不管了,不管有没有事件未处理完都不再添加到 list 就绪链表中。
就有点像下面的流程:
关于触发模式详解,这里面也讲的比较详细: http://blog.csdn.net/weiyuefei/article/details/52242778
- wait返回 -> 清空list就绪链表
- if(LT模式)
- {
- if(存在未处理完的事件)
- {
- 重新添加进list就绪链表中
- }
- }
- else // ET 模式
- {
- }
1)由不可写,变成可写
2)buffer 是数据变少的时候,也就是被读走了一部分 3)buffer 有可写空间,且用户对相应 fd 进行 epoll_mod OUT 事件时。
对于 LT 模式:
读操作:只要缓冲区中有数据,且读完一部分之后还不空的时候,就会返回
写操作:当发送缓冲区没满,写了一下还不满的时候,epoll_wait 返回读事件。
补充一个例子 1:验证 ET 模式的读取返回的前 2 个:
- #include<unistd.h>
- #include<iostream>
- #include<sys/epoll.h>
- using namespace std;
- int main()
- {
- int epfd, ret;
- struct epoll_event ev, events[5];
- epfd = epoll_create(1);
- ev.data.fd = STDIN_FILENO;
- ev.events = EPOLLIN|EPOLLET; // 标记A,这里是ET模式
- //ev.events = EPOLLIN; // 标记B。表示默认是LT模式
- char buf[1024] = {0};
- epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //添加标准输入
- while(1)
- {
- ret = epoll_wait(epfd, events, 5, -1);
- for(int i=0; i < ret; i++)
- {
- if(events[i].data.fd == STDIN_FILENO)
- {
- //read(STDIN_FILENO, buf, sizeof(buf)); // 标记C
- cout << "hello world, recv:" << buf << endl;
- }
- }
- }
- return 0;
- }
分三种情况讨论:
1)打开标记 A,注释 B 和 C:这种情况运行,虽然输入缓冲区里面还有数据,但是 "hello world" 也不会一直打印。
因为边沿触发,一定要等到下一次事件到来 wait 才会返回。
2)打开 B,注释 A 和 C:切换成了 LT 模式,只要缓冲区里面还有数据吗,wait 会一直返回。所以 helloworld 会一直打印
3)打开 B 和 C,注释 A:LT 模式,但是每次 wait 之后把缓冲区里面的数据读完了,相当于处理完了这个事件。wait 就不会返回了。除非标准输入中再输入数据。
例子 2:验证 ET 模式的读取返回的第 3 个:
- #include<unistd.h>
- #include<iostream>
- #include<sys/epoll.h>
- using namespace std;
- int main()
- {
- int epfd, ret;
- struct epoll_event ev, events[5];
- epfd = epoll_create(1);
- ev.data.fd = STDIN_FILENO;
- ev.events = EPOLLIN|EPOLLET;
- char buf[1024] = {0};
- epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);
- while(1)
- {
- ret = epoll_wait(epfd, events, 5, -1);
- for(int i=0; i < ret; i++)
- {
- if(events[i].data.fd == STDIN_FILENO)
- {
- cout << "hello world << endl;
- ev.data.fd = STDIN_FILENO;
- ev.events = EPOLLIN|EPOLLET;
- epoll_ctl(epfd, EPOLL_CTL_MOD, STDIN_FILENO, &ev); // 这里对fd进行epoll_mod IN 事件
- }
- }
- }
- return 0;
- }
可以看到当输入一次之后,依然会有死循环打印 helloworld。
例子 3:验证 ET 模式的写返回,前 2 个
- #include<unistd.h>
- #include<iostream>
- #include<sys/epoll.h>
- using namespace std;
- int main()
- {
- int epfd, ret;
- struct epoll_event ev, events[5];
- epfd = epoll_create(1);
- ev.data.fd = STDIN_FILENO;
- ev.events = EPOLLOUT|EPOLLET;
- char buf[1024] = {0};
- epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);
- while(1)
- {
- ret = epoll_wait(epfd, events, 5, -1);
- for(int i=0; i < ret; i++)
- {
- if(events[i].data.fd == STDIN_FILENO)
- {
- //cout << "hello world" << endl; // 标记A
- cout << "hello world"; // 标记B
- }
- }
- }
- return 0;
- }
对于 ET 模式。
1)打开标记 A,注释标记 B:可以看到会死循环,因为这里有 endl 。标准输出为控制台的时候缓冲的 "行缓冲",所以换行符号导致 buffer 中的内容被清空。就相当于上面条件中的第二个,有数据发送走了。所以会一直循环
2)打开 B,注释 A:不发送 endl,就相当于 buffer 中一直有数据存在,所以 wait 不会一直返回。
例子 4,ET 模式的写返回第三个条件。
- #include<unistd.h>
- #include<iostream>
- #include<sys/epoll.h>
- using namespace std;
- int main()
- {
- int epfd, ret;
- struct epoll_event ev, events[5];
- epfd = epoll_create(1);
- ev.data.fd = STDIN_FILENO;
- ev.events = EPOLLOUT|EPOLLET;
- char buf[1024] = {0};
- epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);
- while(1)
- {
- ret = epoll_wait(epfd, events, 5, -1);
- for(int i=0; i < ret; i++)
- {
- if(events[i].data.fd == STDIN_FILENO)
- {
- cout << "hello world";
- ev.data.fd = STDIN_FILENO;
- ev.events = EPOLLOUT;
- epoll_ctl(epfd, EPOLL_CTL_MOD, STDIN_FILENO, &ev); // 这里对fd进行epoll_mod OUT 事件
- }
- }
- }
- return 0;
- }
每次输出 helloworld 后重新 MOD OUT 事件。也会一直循环打印。
注意:LT 模式没有验证
来源: https://www.cnblogs.com/xcywt/p/8146143.html