epoll的相关系统调用
#include <sys/epoll.h>
int epoll_create(int size);
- epoll_create会创建一个epoll模型,然后返回一个指向该epoll模型的文件描述符。
- 参数已经弃用,为了兼容性才保留。随意填写。
- 成功返回文件描述符,失败返回-1。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t; /* epoll_data_t */
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
- epoll_ctl用于向epoll模型中添加,修改,删除文件描述符的某些事件。所以epoll_ctl是用户告诉内核。
- 第一个参数就是epoll模型的文件描述符,代表你要修改的是哪个epoll模型。
- 第二个参数是你要进行的操作,是添加,修改,还是删除。
- 第三个参数是你想监控的文件描述符。
- 第四个参数是一个epoll_event结构体,代表你想要监控该文件描述符的哪些事件。
- 这里只说两个,EPOLLIN(读事件),EPOLLOUT(写事件)。
struct epoll_event:
- 这个结构体第一个成员变量events就是事件的集合,使用的方法和poll的一样。都是通过按位或添加到events中。通过按位与判断是否含有某种事件。
- 第二个成员变量data是一个epoll_data的联合体。我们知道poll和select需要考虑上层的协议,但是我们不知道究竟读取了多少数据,而epoll给我们提供了方法。
- 我们可以操作epoll_data中的fd指向该事件集合的文件描述符,或者使用ptr实现更复杂的协议。
- 成功返回0,失败返回-1。
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
- epoll_wait是内核告诉用户。
- 第一个参数表示你想等待的epoll模型。
- 第二个参数是一个epoll_event数组,输出型参数,epoll_wait会将就绪事件添加到events数组中。(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。
- maxevents表示你希望有多少事件就绪,或者说你最多能允许有多少事件就绪。(这与read的一个参数类型,代表你的缓冲区大小)
- 最后一个参数和poll的timeout一样。
- 返回值: 返回值和poll的一样,表示实际有多少事件就绪。但是,epoll在返回的时候,是从0号数组下标依次往后的!!也就是说,我们只需要遍历数组的前返回值个事件,就找到了所有就绪事件!!!
epoll的原理
epoll模型:
-
我们在epoll_create的时候,底层会为我们创建一个epoll模型。而这个模型就是一棵空红黑树! 当我们插入,修改,删除文件描述符的时候,底层会帮助我们创建一棵节点,这个节点的key就是文件描述符,而value就是events。
-
我们添加事件的时候,就相当于添加一棵节点到红黑树中;修改,就相当于先找到节点,然后通过key值修改它的value;删除fd,就相当于找到对应的节点, 然后删除。
-
而红黑树的查找效率非常之高!
epoll的回调机制:
- select和poll底层会做大量的轮询检测,因为操作系统需要主动的判断哪些文件描述符的哪些事件就绪。这就导致了它们不能处理大量的文件描述符!
- 而epoll采用回调机制。在epoll_create调用的时候,操作系统不光会创建红黑树,还会创建回调机制!!
- 然后我们调用epoll_ctl的时候,系统不光会创建一棵节点插入到红黑树中,还会事件在对应的驱动程序中注册一个回调函数,当满足条件时,回调函数会通知系统,该事件就绪。
- 这就变成了操作系统被动接收,当文件描述符变多时,效率反而会上升。
栗子:
老师想检查作业,有两种方法。
- 法一:老师主动检查,让同学们将作业放到桌子上,老师亲自到每个同学的座位上去检查。这样效率极低。(select && poll)
- 法二:老师直接说:谁作业写完了拿给我检查,检查完才可以下课。这样老师被动的接受作业,学生们谁写完,谁拿给老师检查。 (epoll)
epoll的就绪队列:
- 比起poll,epoll的另外一个改进就是就绪队列。
- 在poll中,即使拿到就绪事件的集合,我们仍不知道哪些下标代表事件就绪,仍需要轮询检测。但是,epoll增加了就绪队列。
- 当某一个事件就绪时,系统会将该就绪节点添加到就绪队列中,这样返回的时候直接将就绪队列返回即可。
- epoll_create会创建就绪队列。epoll_wait的返回值就是就绪队列的长度!!而判断是否有事件就绪,我们只需要判断就绪队列是否为空,不需要进行轮询检测!! 大大节省了时间。
epoll的优点
- 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开。
- 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)。
- 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中。
- epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响。
- 没有数量限制: 文件描述符数目无上限,而且是文件描述符越多,效率越高。
epoll代码
/* sock.hpp */
1 #pragma once
2
3 #include <iostream>
4 #include <string>
5 #include <unistd.h>
6 #include <sys/types.h>
7 #include <sys/socket.h>
8 #include <netinet/in.h>
9 #include <cstring>
10 #include <sys/epoll.h>
11 #define LOGBACK 5
12 using namespace std;
13 class Sock{ // 这个类用于封装socket
14 public:
15 static int Socket(){
16 int sockfd = socket(AF_INET, SOCK_STREAM, 0);
17 if(sockfd < 0){
18 cerr << "socket error "<< endl;
19 exit(1);
20 }
21 return sockfd;
22 }
23 static void Bind(int sockfd, int port){
24 struct sockaddr_in local;
25 local.sin_family = AF_INET;
26 local.sin_port = htons(port);
27 local.sin_addr.s_addr = htonl(INADDR_ANY);
28 int ret = bind(sockfd, (struct sockaddr*)&local, sizeof(local));
29 if(ret < 0){
30 cerr << "bind error" << endl;
31 exit(2);
32 }
33 }
34 static void Listen(int sockfd){
35 int ret = listen(sockfd, LOGBACK);
36 if(ret < 0){
37 cerr << "listen error" << endl;
38 exit(3);
39 }
40 }
41 static int Accept(int sockfd){
42 struct sockaddr_in peer;
43 socklen_t len = sizeof(peer);
44 int sock = accept(sockfd, (struct sockaddr*)&peer, &len);
45 if(sock < 0){
46 cerr << "accept error" << endl;
47 return -1; // accept wrong, 服务器继续运行
48 }
49 return sock;
50 }
51 static void Setsockopt(int lsock){
52 int opt = 1;
53 setsockopt(lsock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
54 }
55 };
epoll.hpp
/* epoll.hpp */
1 #include "sock.hpp"
2
3 #define SIZE 64
4
5 struct bucket{ // 用于接收每个epoll_event的数据
6 int fd;
7 int pos;
8 char buf[10] = {0};
9
10 bucket(int _fd): fd(_fd), pos(0){}
11 };
12
13 class EpollServer{
14 private:
15 int port;
16 int lsock;
17 int epfd; // epoll的结构
18
19 public:
20 EpollServer(int _port = 8080) 21 :port(_port)
22 ,lsock(-1)
23 ,epfd(-1){}
24
25 void Init(){
26 lsock = Sock::Socket();
27 Sock::Setsockopt(lsock);
28 Sock::Bind(lsock, port);
29 Sock::Listen(lsock);
30 epfd = epoll_create(10); // 参已经弃用;随意设置;
31 if(epfd < 0){
32 cerr << "epoll_create error" << endl;
33 exit(6);
34 }
35 }
36 void Start(){
37 // 将lsock添加到 epoll的结构中;
38 struct epoll_event events;
39 events.events = EPOLLIN;
40 events.data.ptr = new bucket(lsock);
41 //int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
42 epoll_ctl(epfd, EPOLL_CTL_ADD, lsock, &events);
43
44 struct epoll_event ee[SIZE];
45 while(true){
46 int ret = epoll_wait(epfd, ee, SIZE, 1000);
47 if(ret > 0){
48 HandleEvents(ee, ret);
49 }
50 else if(ret == 0){
51 cout << "timeout..." << endl;
52 }
53 else{
54 cerr << "epoll_wait error" << endl;
55 exit(7);
56 }
57 }//服务器的大循环
58 }
59 ~EpollServer(){
60 close(lsock);
61 close(epfd);
62 }
63
64 private: //这里的函数用于epoll类内部使用
65 void HandleEvents(struct epoll_event* pEvents, int num){
66 for(int i = 0; i < num; ++i){
67 if(pEvents[i].events & EPOLLIN){
68 bucket* b_ptr = static_cast<bucket*>(pEvents[i].data.ptr);
69 if(b_ptr->fd == lsock){
70 int sock = Sock::Accept(lsock);
71 AddFd2Epoll(sock, EPOLLIN);
72 }//读取的是一个连接
73 else{
74 ssize_t rest = sizeof(b_ptr->buf) - b_ptr->pos - 1;
75 ssize_t ss = recv(b_ptr->fd, b_ptr->buf + b_ptr->pos, rest, 0);
76 if(ss > 0){
77 if(ss < rest){
78 cout << b_ptr->buf << endl;
79 b_ptr->pos = ss;
80 } //没有读取到我想要的数量,继续读取
81 else{
82 cout << b_ptr->buf << endl;
83 struct epoll_event temp; //epoll_ctl会将数据拷贝到内核,所以不用担心temp是临时变量的> 问题
84 temp.events = EPOLLOUT;
85 temp.data.ptr = b_ptr;
86 epoll_ctl(epfd, EPOLL_CTL_MOD, b_ptr->fd, &temp);
87 }//buf满了,开始分析,写入数据
88 } // 读取到了数据;
89 else if(ss == 0){
90 cout << "client quit..." << endl;
91 DelFdFromEpoll(b_ptr->fd);
92 close(b_ptr->fd);
93 delete b_ptr;
94 }
95 else{
96 cerr << "recv error" << endl;
97 exit(8);
98 }
99 }//读取到的是数据
100 }//读事件就绪
101 else if(pEvents[i].events & EPOLLOUT){
102 bucket* b_ptr = static_cast<bucket*>(pEvents[i].data.ptr);
103 int left = strlen(b_ptr->buf);
104 ssize_t ss = send(b_ptr->fd, b_ptr->buf, strlen(b_ptr->buf), 0);
105 left -= ss;
106 if(left > 0){
107 (b_ptr->buf)[left] = 0;
108 }
109 else{
110 DelFdFromEpoll(b_ptr->fd);
111 close(b_ptr->fd);
112 delete b_ptr;
113 }
114 }//写事件就绪
115 else{
116 // other thing,
117 }
118 }//遍历pEvents
119 } //函数结束
120
121 void AddFd2Epoll(int fd, uint32_t MOD){
122 struct epoll_event event;
123 event.events = MOD;
124 event.data.ptr = new bucket(fd);
125 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
126 }
127 void DelFdFromEpoll(int fd){
128 epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
129 }
130 };
epoll.cc
1 #include "epoll.hpp"
2
3 int main(){
4 EpollServer* es = new EpollServer(8080);
5 es->Init();
6 es->Start();
7
8 delete es;
9 }
epoll的工作方式
- 张三和李四都是快递员。
- 一天张三给小明送快递,张三到小明楼下。然后给小明打电话,此时小明正在玩英雄联盟,快被推到高地了,就告诉张三等一会。然后过一会,张三又打电话催小明去拿快递,小明还没打完,就又等一会,然后张三就不停的打电话。。。
- 李四呢,打一个电话,以后就都不打了。除非小明又来了新的快递。
水平触发,LT
-
张三这种送快递的方式就是水平触发,LT(Level Triggered)
-
epoll默认状态下就是LT工作模式。
-
当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分。
-
比如socket收到2K的数据, 第一次只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait仍然会立刻返回并通知socket读事件就绪。
-
直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回。
-
支持阻塞读写和非阻塞读写。
简单来说:如果底层的数据就绪,那么就一直向上层通知有数据准备好了,快来取,如果没有取完,那么也会不停的发送通知。
边缘触发,ET
- 李四的工作方式就是ET(Edge Triggered)
- 简单来说:底层数据来了,回想上层通知一次,数据准备就绪,快来取,如果没有取或者一次性没有取完,那么epoll_wait就不会再次通知你。除非来了新的数据。
- 当epoll检测到socket上事件就绪时, 必须立刻处理。
- 如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 。epoll_wait 的时候, epoll_wait 不会再返回了。
- 也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会。
- ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll。
- 只支持非阻塞的读写。
其他问题:
- 如果你想要某个事件是ET模式,那么在它socket的events集合中按位或上EPOLLET即可。
- 因为ET模式下epoll_wait返回的次数少了,所以对程序猿要求更高了。我们希望一次性将数据全读上来,这样就减少数据丢失的可能。
怎样判断数据读取完毕呢?
- 问你爸要钱,每次要100.如果你爸给你100,那么证明你爸还有钱。如果你爸只给了你30(不到100),那么就可以说明你爸没钱了。
- 这里同理,我们利用循环读取,假设每次读取512字节数据,当某次读取到的数据小于512,那么证明数据读取完毕。
- but,如果socket里面正好有1024个字节数据,你读取两次512,然后第三次读取的时候,就会被卡死!!
- 但是我们的多路转接是单进程,不允许卡死的情况,所以,ET模式下的文件描述符必须是非阻塞的!!!
LT vs ET
-
LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序员一次响应就绪过程中就把所有的数据都处理完。
-
相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些(简单说为什么ET比LT高效:因为ET的通知方式没有做重复的动作). 但是在 LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的。
-
另一方面, ET 的代码复杂程度更高了(没办法,底层简单,那么代价就是上层复杂)。
-
实际上,我们可以结合ET和LT的工作模式,对不同的套接字采用不同的工作模式。