IO多路复用之 epoll
epoll初识
select和poll会随着文件描述符数量的增多,而使其性能下降;但epoll不会。
所以为了解决select和poll的问题,引入了epoll。
epoll的相关系统调用
epoll有3个相关的系统调用。
epoll_create
int epoll_create(int size);
- 创建一个epoll的句柄,此处的句柄实际上就是文件描述符。因为epoll的生命周期随进程;而消息队列、共享内存中创建的句柄,不是文件描述符,因为消息队列和共享内存的生命周期是随内核的,即:进程退出,但资源还存在。而文件描述符会随着进程的退出而消失。
- 自从linux2.6.8之后,size参数是被忽略的。但size还存在的原因是,要达到向前兼容的目的,以防之前写的代码不能用。
- 调用完该函数后,记得调用 close() 关闭。
epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
- 第一个参数是epoll_create()的返回值(epoll的句柄);
- 第二个参数表示要对文件描述符进行具体的什么样的操作,用三个宏来表示;
- 第三个参数是需要监听的fd,表示要对该文件描述符进行操作。
- 第四个参数告诉内核需要监听什么事。
第二个参数的取值:
- EPOLL_CTL_ADD:注册新的fd到epfd中;
- EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
- EPOLL_CTL_DEL:从epfd中删除一个fd.
struct epoll_event 结构体:
The struct epoll_event is defined as :
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下几个宏的集合:
- EPOLLIN:表示对应的文件描述符可以读;
- EPOLLOUT:表示对应的文件描述符可以写;
- EPOLLPRI:表示对应的文件描述符有紧急的数据可以读;
epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数解释:
- 参数events是分配好的epoll_event结构体数组;
- epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到)
- 参数timeout 是超时时间(毫秒,0表示立即返回,-1表示永久阻塞);
- 如果函数调用成功,返回对应IO上已经准备好的文件描述符数目;如果返回0,表示已超时;如果返回值小于0,表示函数失败。
epoll 工作原理
- 每⼀个epoll对象都有⼀个独⽴的eventpoll结构体,⽤于存放通过epoll_ctl⽅法向epoll对象中添加进来的事件.
- 这些事件都会挂载在红⿊树中,如此,重复添加的事件就可以通过红⿊树⽽⾼效的识别出来(红⿊树的插⼊时间效率是lgn,其中n为树的⾼度).
- ⽽所有添加到epoll中的事件都会与设备(网卡)驱动程序建⽴回调关系,也就是说,当响应的事件发⽣时会调⽤这个回调⽅法.
- 这个回调⽅法在内核中叫eppollcallback,它会将发⽣的事件添加到rdlist双链表中.
- 在epoll中,对于每⼀个事件,都会建⽴⼀个epitem结构体。
- 当调⽤epoll_wait检查是否有事件发⽣时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可.
- 如果rdlist不为空,则把发⽣的事件复制到⽤户态,同时将事件数量返回给⽤户. 这个操作的时间复杂度是O(1)。
epoll的使用步骤:
- 调用 epoll_create 创建一个epoll句柄;
- 调用 epoll_ctl ,将要监控的文件描述符进行注册;
- 调用 epoll_wait,等待文件描述符就绪。
epoll 的优点
- 从接口角度上讲:不必每次都重新设置文件描述符,使接口使用更加方便,也能够避免频繁地用户态和内核态之间拷贝文件描述符的结构;
- 基于事件的就绪通知方式:一旦被监听的某个文件描述符就绪,内核就会采取回调机制(而select和poll采取的是轮询机制),迅速激活这个文件描述符。这样随着文件描述符数量的增加,也不会影响就绪的性能;
- 维护就绪队列:当文件描述符就绪,就会被放到内核中的就绪队列中。这样调用epoll_wait获取就绪文件描述符时,就可以直接从这个就绪队列中取,操作的时间复杂度位O(1),相比于select和poll的时间复杂度为O(N).
- 文件描述符无上限:通过 epoll_ctl() 来注册一个文件描述符,内核中通过红黑树来管理所有需要监控的文件描述符。
注意:struct epoll_event 是我们在用户空间中分配好的内存,所以还是要将内核的数据拷贝到这个用户空间的内存空间中。
epoll 工作方式
水平触发方式(LT):
再次调用 epoll_wait,epoll_wait 就会立即返回,提示还有同一个未读完的文件描述符就绪。
边缘触发方式(ET):
再次调用 epoll_wait,epoll_wait 就不会返回,此时缓冲区中剩下的1K数据就不能立刻被读取到。就需要等到对应的socket再次收到数据触发读就绪,才有机会将之前残留的1K数据读出来。
注:
- epoll 默认的方式是水平触发。通过设置选项EPOLLET标记,才可以将epoll设置成边缘触发。
- select 和 poll 只有水平触发。
- 边缘触发要求我们一旦文件描述符就绪,必须把缓冲区中所有数据的读出来,以防后续数据丢失。所以,边缘触发的效率高,也将此方式称为高速模式。
- 边缘触发(ET)要将fd设置成非阻塞轮询的方式进行操作。
epoll 的使用场景
对于多连接,且多连接中只有一部分连接比较活跃时,才适合使用epoll。
例如:
典型的一个需要处理上万个客户端的服务器,例如各种互联网APP的入口服务器,这样的服务器就很适合epoll。
epoll 示例:epoll 服务器
1 /////////////////////////////////////////////////////////
2 // 基于 epoll 实现一个 TCP 服务器(回显服务器)
3 ////////////////////////////////////////////////////////
4 #include <stdio.h>
5 #include <string.h>
6 #include <stdlib.h>
7 #include <unistd.h>
8 #include <sys/socket.h>
9 #include <sys/epoll.h>
10 #include <netinet/in.h>
11 #include <arpa/inet.h>
12
13 typedef struct sockaddr sockaddr;
14 typedef struct sockaddr_in sockaddr_in;
15 typedef struct epoll_event epoll_event;
16
17 int ServerInit(const char* ip, short port)
18 {
19 // 1.创建 socket
20 int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
21 if(listen_sock < 0)
22 {
23 perror("socket");
24 return -1;
25 }
26 // 2.绑定端口号
27 sockaddr_in addr;
28 addr.sin_family = AF_INET;
29 addr.sin_addr.s_addr = inet_addr(ip);
30 addr.sin_port = htons(port);
31 int ret = bind(listen_sock, (sockaddr*)&addr, sizeof(addr));
32 if(ret < 0)
33 {
34 perror("bind");
35 return -1;
36 }
37 // 3.监听
38 ret = listen(listen_sock, 5);
39 if(ret < 0)
40 {
41 perror("listen");
42 return -1;
43 }
44 return listen_sock;
45 }
46
47 void ProcessListenSock(int epoll_fd, int listen_sock)
48 {
49 // 1.调用 accpet
50 sockaddr_in peer;
51 socklen_t len = sizeof(peer);
52 int new_sock = accept(listen_sock, (sockaddr*)&peer, &len);
53 if(new_sock < 0)
54 {
55 perror("accept");
56 return;
57 }
58 // 2.把 new_sock 加入到 epoll 之中
59 epoll_event event;
60 event.events = EPOLLIN;
61 // 一定要记得带上这里的文件描述符!
62 event.data.fd = new_sock;
63 int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_sock, &event);
64 if(ret < 0)
65 {
66 perror("epoll_ctl ADD");
67 return;
68 }
69 printf("[client %d] connected!\n",new_sock);
70 return;
71 }
72
73 void ProcessNewSock(int epoll_fd, int new_sock )
74 {
75 // 1.进行读写数据
76 char buf[1024]={0};
77 ssize_t read_size = read(new_sock, buf, sizeof(buf)-1);
78 if(read_size < 0)
79 {
80 perror("read");
81 return;
82 }
83 if(read_size ==0)
84 {
85 // 2.一旦读到返回值为0,对端关闭了文件描述符。
86 // 本端也应该关闭文件描述符。并且,也应该把
87 // 文件描述符从epoll之中删除掉。
88 close(new_sock);
89 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, new_sock, NULL);
90 printf("[client %d] disconnected!\n", new_sock);
91 return;
92 }
93 buf[read_size]='\0';
94 printf("[client %d] say: %s\n", new_sock, buf);
95 write(new_sock, buf, strlen(buf));
96 return;
97 }
98 int main(int argc, char* argv[])
99 {
100 if(argc != 3)
101 {
102 printf("Usage /.server_epoll [ip] [port]\n");
103 return 1;
104 }
105
106 // 1.创建并初始化 socket
107 int listen_sock = ServerInit(argv[1], atoi(argv[2]));
108 if(listen_sock < 0)
109 {
110 printf("ServerInit failed\n");
111 return 1;
112 }
113
114 // 2.创建并初始化 epoll 对象
115 // 把 listen_sock 放到 epoll 对象之中
116 int epoll_fd = epoll_create(10);
117 if(epoll_fd < 0)
118 {
119 perror("epoll_create");
120 return 1;
121 }
122 epoll_event event;
123 event.events = EPOLLIN;
124 // 由于epoll_event 结构体在返回时,只返回了值,
125 // 而没有返回具体的文件描述符的种类(即键值对的键),
126 // 所以,为了后续操作中的需要,将文件描述符手动保存到
127 // epoll_event 结构体的data.fd中。
128 event.data.fd = listen_sock;
129 int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD,
130 listen_sock, &event);
131 if(ret < 0)
132 {
133 perror("epoll_ctl");
134 return 1;
135 }
136 // 服务器初始化完成
137 printf("ServerInit OK\n");
138
139 // 3.进入循环
140 while(1)
141 {
142 epoll_event output_event[100];
143 int nfds = epoll_wait(epoll_fd, output_event, 100, -1);
144 if(nfds < 0)
145 {
146 perror("epoll_wait");
147 continue;
148 }
149 // epoll_wait 返回之后,都有哪些文件描述符就绪,
150 // 就写到了 output_event 中了。遍历该缓冲区,由于
151 // epoll 适用场景:同一时刻有大量的客户端连接,但活跃的数量比较少
152 // 所以缓冲区里就绪的文件描述符较少,所以遍历该缓冲区时,效率也是很高的。
153 int i=0;
154 for(; i < nfds; ++i)
155 {
156 // 根据就绪的文件描述符的类别,分情况讨论
157 if(listen_sock == output_event[i].data.fd)
158 {
159 // a) listen_sock 就绪,调用accept
160 ProcessListenSock(epoll_fd, listen_sock);
161 }
162 else
163 {
164 // b) new_sock 就绪,调用一次read/write
165 ProcessNewSock(epoll_fd, output_event[i].data.fd);
166 } // end if
167 } // end for
168 } // end while(1)
169
170 return 0;
171 }