1、epoll 系列函数简介html
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
linux
* epoll_create(2) creates an epoll instance and returns a file descriptor referring to that instance. (The more recent
epoll_create1(2) extends the functionality of epoll_create(2).)
* Interest in particular file descriptors is then registered via epoll_ctl(2). The set of file descriptors currently
registered on an epoll instance is sometimes called an epoll set.
* epoll_wait(2) waits for I/O events, blocking the calling thread if no events are currently available.
ios
一、epoll_create1 产生一个epoll 实例,返回的是实例的句柄。flag 能够设置为0 或者EPOLL_CLOEXEC,为0时函数表现与epoll_create一致,EPOLL_CLOEXEC标志与open 时的O_CLOEXEC 标志相似,即进程被替换时会关闭打开的文件描述符。c++
二、epoll_ctl :程序员
(1)epfd:epoll 实例句柄;编程
(2)op:对文件描述符fd 的操做,主要有EPOLL_CTL_ADD、 EPOLL_CTL_DEL等;ubuntu
(3)fd:须要操做的目标文件描述符;数组
(4)event:结构体指针安全
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、EPOLLET、EPOLLLT等;通常data 共同体咱们设置其成员fd便可,也就是epoll_ctl 函数的第三个参数。
三、epoll_wait:
(1)epfd:epoll 实例句柄;
(2)events:结构体指针
(3)maxevents:事件的最大个数
(4)timeout:超时时间,设为-1表示永不超时
下面咱们使用c++ 来实现一个服务器端程序:
C++ Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
|
#include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <fcntl.h> #include <sys/wait.h> #include <sys/epoll.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #include <vector> #include <algorithm> #include "read_write.h" #include "sysutil.h" typedef std::vector<struct epoll_event> EventList; /* 相比于select与poll,epoll最大的好处是不会随着关心的fd数目的增多而下降效率 */ int main(void) { int count = 0; int listenfd; if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) ERR_EXIT("socket"); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(5188); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); int on = 1; if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) ERR_EXIT("setsockopt"); if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) ERR_EXIT("bind"); if (listen(listenfd, SOMAXCONN) < 0) ERR_EXIT("listen"); std::vector<int> clients; int epollfd; epollfd = epoll_create1(EPOLL_CLOEXEC); //epoll实例句柄 struct epoll_event event; event.data.fd = listenfd; event.events = EPOLLIN | EPOLLET; //边沿触发 epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event); EventList events(16); struct sockaddr_in peeraddr; socklen_t peerlen; int conn; int i; int nready; while (1) { nready = epoll_wait(epollfd, &*events.begin(), static_cast<int>(events.size()), -1); if (nready == -1) { if (errno == EINTR) continue; ERR_EXIT("epoll_wait"); } if (nready == 0) continue; if ((size_t)nready == events.size()) events.resize(events.size() * 2); for (i = 0; i < nready; i++) { if (events[i].data.fd == listenfd) { peerlen = sizeof(peeraddr); conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen); if (conn == -1) ERR_EXIT("accept"); printf("ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port)); printf("count = %d\n", ++count); clients.push_back(conn); activate_nonblock(conn); event.data.fd = conn; event.events = EPOLLIN | EPOLLET; epoll_ctl(epollfd, EPOLL_CTL_ADD, conn, &event); } else if (events[i].events & EPOLLIN) { conn = events[i].data.fd; if (conn < 0) continue; char recvbuf[1024] = {0}; int ret = readline(conn, recvbuf, 1024); if (ret == -1) ERR_EXIT("readline"); if (ret == 0) { printf("client close\n"); close(conn); event = events[i]; epoll_ctl(epollfd, EPOLL_CTL_DEL, conn, &event); clients.erase(std::remove(clients.begin(), clients.end(), conn), clients.end()); } fputs(recvbuf, stdout); writen(conn, recvbuf, strlen(recvbuf)); } } } return 0; }
|
在程序的最开始定义一个新类型EventList,内部装着struct epoll_event 结构体的容器。
接下面的socket,bind,listen 都跟之前说的同样,不述。接着使用epoll_create1 建立一个epoll 实例,再来看下面四行代码:
struct epoll_event event;
event.data.fd = listenfd;
event.events = EPOLLIN | EPOLLET; //边沿触发
epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event);
根据前面的函数分析,这四句意思就是将监听套接字listenfd 加入关心的套接字序列。
在epoll_wait 函数中的第二个参数,其实events.begin() 是个迭代器,但其具体实现也是struct epoll_event* 类型,虽然 &*events.begin() 获得的也是struct epoll_event* ,但不能直接使用events.begin() 作参数,由于类型不匹配,编译会出错。
EventList events(16); 即初始化容器的大小为16,当返回的事件个数nready 已经等于16时,须要增大容器的大小,使用events.resize 函数便可,容器能够动态增大,这也是咱们使用c++实现的其中一个缘由。
当监听套接字有可读事件,accept 返回的conn也须要使用epoll_ctl 函数将其加入关心的套接字队列。
还须要调用 activate_nonblock(conn); 将conn 设置为非阻塞,man 7 epoll 里有这样一句话:
An application that employs the EPOLLET flag should use nonblocking file descriptors to avoid having a blocking read or
write starve a task that is handling multiple file descriptors.
当下次循环回来某个已链接套接字有可读事件,则读取数据,若read 返回0表示对方关闭,须要使用epoll_ctl 函数将conn 从队列中清除,咱们使用 std::vector<int> clients; 来保存每次accept 返回的conn,因此如今也须要将其擦除掉,调用clients.erase() 函数。
咱们可使用前面写的conntest 客户端程序测试一下,先运行服务器程序,再运行客户端,输出以下:
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echoser_epoll
................................
count = 1015
ip=127.0.0.1 port=60492
count = 1016
ip=127.0.0.1 port=60493
count = 1017
ip=127.0.0.1 port=60494
count = 1018
ip=127.0.0.1 port=60495
count = 1019
accept: Too many open files
simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./conntest
.........................................................
count = 1015
ip=127.0.0.1 port=60492
count = 1016
ip=127.0.0.1 port=60493
count = 1017
ip=127.0.0.1 port=60494
count = 1018
ip=127.0.0.1 port=60495
count = 1019
connect: Connection reset by peer
为何服务器端的count 只有1019呢,由于除去012,一个监听套接字还有一个epoll 实例句柄,因此1024 - 5 = 1019。
为何客户端的错误提示跟这里的不同呢?这正说明epoll 处理效率比poll和select 都高,由于处理得快,来一个链接就accept一个,当服务器端accept 完第1019个链接,再次accept 时会由于文件描述符总数超出限制,打印错误提示,而此时客户端虽然已经建立了第1020个sock,但在connect 过程当中发现对等方已经退出了,故打印错误提示,链接被对等方重置。若是服务器端处理得慢的话,那么客户端会connect 成功1021个链接,而后在建立第1022个sock 的时候出错,打印错误提示:socket: Too many open files,固然由于文件描述符的限制,服务器端也只能从已完成链接队列中accept 成功1019个链接。
2、epoll与select、poll区别
一、相比于select与poll,epoll最大的好处在于它不会随着监听fd数目的增加而下降效率。内核中的select与poll的实现是采用轮询来处理的,轮询的fd数目越多,天然耗时越多。
二、epoll的实现是基于回调的,若是fd有指望的事件发生就经过回调函数将其加入epoll就绪队列中,也就是说它只关心“活跃”的fd,与fd数目无关。
三、epoll不只会告诉应用程序有I/0 事件到来,还会告诉应用程序相关的信息,这些信息是应用程序填充的,所以根据这些信息应用程序就能直接定位到事件,而没必要遍历整个fd集合。
四、当已链接的套接字数量不太大,而且这些套接字都很是活跃,那么对于epoll 来讲一直在调用callback 函数(epoll 内部的实现更复杂,更复杂的代码逻辑),可能性能没有poll 和 select 好,由于一次性遍历对活跃的文件描述符处理,在链接数量不大的状况下,性能更好,但在处理大量链接的状况时,epoll 明显占优。
3、epoll 的EPOLLLT (电平触发,默认)和 EPOLLET(边沿触发)模式的区别
一、EPOLLLT:彻底靠kernel epoll驱动,应用程序只须要处理从epoll_wait返回的fds,这些fds咱们认为它们处于就绪状态。此时epoll能够认为是更快速的poll。
二、EPOLLET:此模式下,系统仅仅通知应用程序哪些fds变成了就绪状态,一旦fd变成就绪状态,epoll将再也不关注这个fd的任何状态信息,(从epoll队列移除)直到应用程序经过读写操做(非阻塞)触发EAGAIN状态,epoll认为这个fd又变为空闲状态,那么epoll又从新关注这个fd的状态变化(从新加入epoll队列)。随着epoll_wait的返回,队列中的fds是在减小的,因此在大并发的系统中,EPOLLET更有优点,可是对程序员的要求也更高,由于有可能会出现数据读取不完整的问题,举例以下:
假设如今对方发送了2k的数据,而咱们先读取了1k,而后这时调用了epoll_wait,若是是边沿触发,那么这个fd变成就绪状态就会从epoll 队列移除,极可能epoll_wait 会一直阻塞,忽略还没有读取的1k数据,与此同时对方还在等待着咱们发送一个回复ack,表示已经接收到数据;若是是电平触发,那么epoll_wait 还会检测到可读事件而返回,咱们能够继续读取剩下的1k 数据。
注:上述使用 epoll ET 的例子只是个示例,更规范的用法能够参考这里。
参考:
《Linux C 编程一站式学习》
《TCP/IP详解 卷一》
《UNP》
select, poll和epoll的区别
随着2.6内核对epoll的彻底支持,网络上不少的文章和示例代码都提供了这样一个信息:使用epoll代替传统的poll能给网络服务应用带来性能上的提高。但大多文章里关于性能提高的缘由解释的较少,这里我将试分析一下内核(2.6.21.1)代码中poll与epoll的工做原理,而后再经过一些测试数据来对比具体效果。
POLL:
先说poll,poll或select为大部分Unix/Linux程序员所熟悉,这俩个东西原理相似,性能上也不存在明显差别,但select对所监控的文件描述符数量有限制,因此这里选用poll作说明。
poll是一个系统调用,其内核入口函数为sys_poll,sys_poll几乎不作任何处理直接调用do_sys_poll,do_sys_poll的执行过程能够分为三个部分:
1,将用户传入的pollfd数组拷贝到内核空间,由于拷贝操做和数组长度相关,时间上这是一个O(n)操做,这一步的代码在do_sys_poll中包括从函数开始到调用do_poll前的部分。
2,查询每一个文件描述符对应设备的状态,若是该设备还没有就绪,则在该设备的等待队列中加入一项并继续查询下一设备的状态。查询完全部设备后若是没有一个设备就绪,这时则须要挂起当前进程等待,直到设备就绪或者超时,挂起操做是经过调用schedule_timeout执行的。设备就绪后进程被通知继续运行,这时再次遍历全部设备,以查找就绪设备。这一步由于两次遍历全部设备,时间复杂度也是O(n),这里面不包括等待时间。相关代码在do_poll函数中。
3,将得到的数据传送到用户空间并执行释放内存和剥离等待队列等善后工做,向用户空间拷贝数据与剥离等待队列等操做的的时间复杂度一样是O(n),具体代码包括do_sys_poll函数中调用do_poll后到结束的部分。
EPOLL:
接下来分析epoll,与poll/select不一样,epoll再也不是一个单独的系统调用,而是由epoll_create/epoll_ctl/epoll_wait三个系统调用组成,后面将会看到这样作的好处。
先来看sys_epoll_create(epoll_create对应的内核函数),这个函数主要是作一些准备工做,好比建立数据结构,初始化数据并最终返回一个文件描述符(表示新建立的虚拟epoll文件),这个操做能够认为是一个固定时间的操做。
epoll是作为一个虚拟文件系统来实现的,这样作至少有如下两个好处:
1,能够在内核里维护一些信息,这些信息在屡次epoll_wait间是保持的,好比全部受监控的文件描述符。
2, epoll自己也能够被poll/epoll;
具体epoll的虚拟文件系统的实现和性能分析无关,再也不赘述。
在sys_epoll_create中还能看到一个细节,就是epoll_create的参数size在现阶段是没有意义的,只要大于零就行。
接着是sys_epoll_ctl(epoll_ctl对应的内核函数),须要明确的是每次调用sys_epoll_ctl只处理一个文件描述符,这里主要描述当op为EPOLL_CTL_ADD时的执行过程,sys_epoll_ctl作一些安全性检查后进入ep_insert,ep_insert里将 ep_poll_callback作为回掉函数加入设备的等待队列(假定这时设备还没有就绪),因为每次poll_ctl只操做一个文件描述符,所以也能够认为这是一个O(1)操做
ep_poll_callback函数很关键,它在所等待的设备就绪后被系统回掉,执行两个操做:
1,将就绪设备加入就绪队列,这一步避免了像poll那样在设备就绪后再次轮询全部设备找就绪者,下降了时间复杂度,由O(n)到O(1);
2,唤醒虚拟的epoll文件;
最后是sys_epoll_wait,这里实际执行操做的是ep_poll函数。该函数等待将进程自身插入虚拟epoll文件的等待队列,直到被唤醒(见上面ep_poll_callback函数描述),最后执行ep_events_transfer将结果拷贝到用户空间。因为只拷贝就绪设备信息,因此这里的拷贝是一个O(1)操做。
还有一个让人关心的问题就是epoll对EPOLLET的处理,即边沿触发的处理,粗略看代码就是把一部分水平触发模式下内核作的工做交给用户来处理,直觉上不会对性能有太大影响,感兴趣的朋友欢迎讨论。
POLL/EPOLL对比:
表面上poll的过程能够看做是由一次epoll_create/若干次epoll_ctl/一次epoll_wait/一次close等系统调用构成,实际上epoll将poll分红若干部分实现的缘由正是由于服务器软件中使用poll的特色(好比Web服务器):
1,须要同时poll大量文件描述符;
2,每次poll完成后就绪的文件描述符只占全部被poll的描述符的不多一部分。
3,先后屡次poll调用对文件描述符数组(ufds)的修改只是很小;
传统的poll函数至关于每次调用都重起炉灶,从用户空间完整读入ufds,完成后再次彻底拷贝到用户空间,另外每次poll都须要对全部设备作至少作一次加入和删除等待队列操做,这些都是低效的缘由。
epoll将以上状况都细化考虑,不须要每次都完整读入输出ufds,只需使用epoll_ctl调整其中一小部分,不须要每次epoll_wait都执行一次加入删除等待队列操做,另外改进后的机制使的没必要在某个设备就绪后搜索整个设备数组进行查找,这些都能提升效率。另外最明显的一点,从用户的使用来讲,使用epoll没必要每次都轮询全部返回结果已找出其中的就绪部分,O(n)变O(1),对性能也提升很多。
此外这里还发现一点,是否是将epoll_ctl改为一次能够处理多个fd(像semctl那样)会提升些许性能呢?特别是在假设系统调用比较耗时的基础上。不过关于系统调用的耗时问题还会在之后分析。
POLL/EPOLL测试数据对比:
测试的环境:我写了三段代码来分别模拟服务器,活动的客户端,僵死的客户端,服务器运行于一个自编译的标准2.6.11内核系统上,硬件为 PIII933,两个客户端各自运行在另外的PC上,这两台PC比服务器的硬件性能要好,主要是保证能轻易让服务器满载,三台机器间使用一个100M交换机链接。
服务器接受并poll全部链接,若是有request到达则回复一个response,而后继续poll。
活动的客户端(Active Client)模拟若干并发的活动链接,这些链接不间断的发送请求接受回复。
僵死的客户端(zombie)模拟一些只链接但不发送请求的客户端,其目的只是占用服务器的poll描述符资源。
测试过程:保持10个并发活动链接,不断的调整僵并发链接数,记录在不一样比例下使用poll与epoll的性能差异。僵死并发链接数根据比例分别是:0,10,20,40,80,160,320,640,1280,2560,5120,10240。
下图中横轴表示僵死并发链接与活动并发链接之比,纵轴表示完成40000次请求回复所花费的时间,以秒为单位。红色线条表示poll数据,绿色表示 epoll数据。能够看出,poll在所监控的文件描述符数量增长时,其耗时呈线性增加,而epoll则维持了一个平稳的状态,几乎不受描述符个数影响。
在监控的全部客户端都是活动时,poll的效率会略高于epoll(主要在原点附近,即僵死并发链接为0时,图上不易看出来),究竟epoll实现比poll复杂,监控少许描述符并不是它的长处。

测试代码及具体数据能够从这里得到,欢迎讨论。
select()系统调用提供一个机制来实现同步多元I/O:
#include <sys/time.h> #include <sys/types.h> #include <unistd.h>
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
FD_CLR(int fd, fd_set *set); FD_ISSET(int fd, fd_set *set); FD_SET(int fd, fd_set *set); FD_ZERO(fd_set *set);
|
调用select()将阻塞,直到指定的文件描述符准备好执行I/O,或者可选参数timeout指定的时间已通过去。
监视的文件描述符分为三类set,每一种对应等待不一样的事件。readfds中列出的文件描述符被监视是否有数据可供读取(若是读取操做完成则不会阻塞)。writefds中列出的文件描述符则被监视是否写入操做完成而不阻塞。最后,exceptfds中列出的文件描述符则被监视是否发生异常,或者没法控制的数据是否可用(这些状态仅仅应用于套接字)。这三类set能够是NULL,这种状况下select()不监视这一类事件。
select()成功返回时,每组set都被修改以使它只包含准备好I/O的文件描述符。例如,假设有两个文件描述符,值分别是7和9,被放在readfds中。当select()返回时,若是7仍然在set中,则这个文件描述符已经准备好被读取而不会阻塞。若是9已经不在set中,则读取它将可能会阻塞(我说多是由于数据可能正好在select返回后就可用,这种状况下,下一次调用select()将返回文件描述符准备好读取)。
第一个参数n,等于全部set中最大的那个文件描述符的值加1。所以,select()的调用者负责检查哪一个文件描述符拥有最大值,而且把这个值加1再传递给第一个参数。
timeout参数是一个指向timeval结构体的指针,timeval定义以下:
#include <sys/time.h> struct timeval { long tv_sec; /* seconds */ long tv_usec; /* 10E-6 second */ }; |
若是这个参数不是NULL,则即便没有文件描述符准备好I/O,select()也会在通过tv_sec秒和tv_usec微秒后返回。当select()返回时,timeout参数的状态在不一样的系统中是未定义的,所以每次调用select()以前必须从新初始化timeout和文件描述符set。实际上,当前版本的Linux会自动修改timeout参数,设置它的值为剩余时间。所以,若是timeout被设置为5秒,而后在文件描述符准备好以前通过了3秒,则这一次调用select()返回时tv_sec将变为2。
若是timeout中的两个值都设置为0,则调用select()将当即返回,报告调用时全部未决的事件,但不等待任何随后的事件。
文件描述符set不会直接操做,通常使用几个助手宏来管理。这容许Unix系统以本身喜欢的方式来实现文件描述符set。但大多数系统都简单地实现set为位数组。FD_ZERO移除指定set中的全部文件描述符。每一次调用select()以前都应该先调用它。
fd_set writefds;
FD_ZERO(&writefds);
FD_SET添加一个文件描述符到指定的set中,FD_CLR则从指定的set中移除一个文件描述符:
FD_SET(fd, &writefds); /* add 'fd' to the set */
FD_CLR(fd, &writefds); /* oops, remove 'fd' from the set */
设计良好的代码应该永远不使用FD_CLR,并且实际状况中它也确实不多被使用。
FD_ISSET测试一个文件描述符是否指定set的一部分。若是文件描述符在set中则返回一个非0整数,不在则返回0。FD_ISSET在调用select()返回以后使用,测试指定的文件描述符是否准备好相关动做:
if (FD_ISSET(fd, &readfds))
/* 'fd' is readable without blocking! */
由于文件描述符set是静态建立的,它们对文件描述符的最大数目强加了一个限制,可以放进set中的最大文件描述符的值由FD_SETSIZE指定。在Linux中,这个值是1024。本章后面咱们还将看到这个限制的衍生物。
返回值和错误代码
select()成功时返回准备好I/O的文件描述符数目,包括全部三个set。若是提供了timeout,返回值多是0;错误时返回-1,而且设置errno为下面几个值之一:
EBADF
给某个set提供了无效文件描述符。
EINTR
等待时捕获到信号,能够从新发起调用。
EINVAL
参数n为负数,或者指定的timeout非法。
ENOMEM
不够可用内存来完成请求。
--------------------------------------------------------------------------------------------------------------
poll()系统调用是System V的多元I/O解决方案。它解决了select()的几个不足,尽管select()仍然常用(多数仍是出于习惯,或者打着可移植的名义):
#include <sys/poll.h> int poll (struct pollfd *fds, unsigned int nfds, int timeout); |
和select()不同,poll()没有使用低效的三个基于位的文件描述符set,而是采用了一个单独的结构体pollfd数组,由fds指针指向这个组。pollfd结构体定义以下:
#include <sys/poll.h>
struct pollfd { int fd; /* file descriptor */ short events; /* requested events to watch */ short revents; /* returned events witnessed */ }; |
每个pollfd结构体指定了一个被监视的文件描述符,能够传递多个结构体,指示poll()监视多个文件描述符。每一个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操做结果事件掩码。内核在调用返回时设置这个域。events域中请求的任何事件均可能在revents域中返回。合法的事件以下:
POLLIN
有数据可读。
POLLRDNORM
有普通数据可读。
POLLRDBAND
有优先数据可读。
POLLPRI
有紧迫数据可读。
POLLOUT
写数据不会致使阻塞。
POLLWRNORM
写普通数据不会致使阻塞。
POLLWRBAND
写优先数据不会致使阻塞。
POLLMSG
SIGPOLL消息可用。
此外,revents域中还可能返回下列事件:
POLLER
指定的文件描述符发生错误。
POLLHUP
指定的文件描述符挂起事件。
POLLNVAL
指定的文件描述符非法。
这些事件在events域中无心义,由于它们在合适的时候老是会从revents中返回。使用poll()和select()不同,你不须要显式地请求异常状况报告。
POLLIN | POLLPRI等价于select()的读事件,POLLOUT | POLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORM | POLLRDBAND,而POLLOUT则等价于POLLWRNORM。
例如,要同时监视一个文件描述符是否可读和可写,咱们能够设置events为POLLIN | POLLOUT。在poll返回时,咱们能够检查revents中的标志,对应于文件描述符请求的events结构体。若是POLLIN事件被设置,则文件描述符能够被读取而不阻塞。若是POLLOUT被设置,则文件描述符能够写入而不致使阻塞。这些标志并非互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操做都会正常返回而不阻塞。
timeout参数指定等待的毫秒数,不管I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时;timeout为0指示poll调用当即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种状况下,poll()就像它的名字那样,一旦选举出来,当即返回。
返回值和错误代码
成功时,poll()返回结构体中revents域不为0的文件描述符个数;若是在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:
EBADF
一个或多个结构体中指定的文件描述符无效。
EFAULT
fds指针指向的地址超出进程的地址空间。
EINTR
请求的事件以前产生一个信号,调用能够从新发起。
EINVAL
nfds参数超出PLIMIT_NOFILE值。
ENOMEM
可用内存不足,没法完成请求。
--------------------------------------------------------------------------------------------------------------
以上内容来自《OReilly.Linux.System.Programming - Talking.Directly.to.the.Kernel.and.C.Library.2007》
--------------------------------------------------------------------------------------------------------------
epoll的优势:
1.支持一个进程打开大数目的socket描述符(FD)
select 最不能忍受的是一个进程所打开的FD是有必定限制的,由FD_SETSIZE设置,默认值是2048。对于那些须要支持的上万链接数目的IM服务器来讲显然太少了。这时候你一是能够选择修改这个宏而后从新编译内核,不过资料也同时指出这样会带来网络效率的降低,二是能够选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面建立进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,因此也不是一种完美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大能够打开文件的数目,这个数字通常远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目能够cat /proc/sys/fs/file-max察看,通常来讲这个数目和系统内存关系很大。
2.IO效率不随FD数目增长而线性降低
传统的select/poll另外一个致命弱点就是当你拥有一个很大的socket集合,不过因为网络延时,任一时间只有部分的socket是"活跃"的,可是select/poll每次调用都会线性扫描所有的集合,致使效率呈现线性降低。可是epoll不存在这个问题,它只会对"活跃"的socket进行操做---这是由于在内核实现中epoll是根据每一个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其余idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,由于这时候推进力在os内核。在一些 benchmark中,若是全部的socket基本上都是活跃的---好比一个高速LAN环境,epoll并不比select/poll有什么效率,相反,若是过多使用epoll_ctl,效率相比还有稍微的降低。可是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
3.使用mmap加速内核与用户空间的消息传递。
这点实际上涉及到epoll的具体实现了。不管是select,poll仍是epoll都须要内核把FD消息通知给用户空间,如何避免没必要要的内存拷贝就很重要,在这点上,epoll是经过内核于用户空间mmap同一块内存实现的。而若是你想我同样从2.5内核就关注epoll的话,必定不会忘记手工 mmap这一步的。
4.内核微调
这一点其实不算epoll的优势了,而是整个linux平台的优势。也许你能够怀疑linux平台,可是你没法回避linux平台赋予你微调内核的能力。好比,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么能够在运行时期动态调整这个内存pool(skb_head_pool)的大小--- 经过echo XXXX>/proc/sys/net/core/hot_list_length完成。再好比listen函数的第2个参数(TCP完成3次握手的数据包队列长度),也能够根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每一个数据包自己大小却很小的特殊系统上尝试最新的NAPI网卡驱动架构。
epoll学习,epoll与select,pool区别
在linux网络编程中,很长的时间都是用select来作事件触发.在linux新内核中,有了一种替换它的机制,就是epoll.
相比于select,epoll最大的好处在于它不会随着监听fd数目的增加而下降效率.由于在内核中的select实现中,它是采用轮询来处理的,轮询的fd数据数据越多,天然耗时就越多.
epoll的接口三个函数
1) int epoll_create(int size);
建立一个epoll句柄,size用来告诉内核这个监听的数据一共有多大.
须要注意的是,当建立好epoll句柄后,它会占用一个fd值,在使用完epoll后,必须调用close关闭,不然可能致使fd被耗尽.
2) int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
epoll注册函数,第一个参数是epoll_create()的返回值.
第二个参数表示动做,用三个宏来表示
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD: 修改已经注册的fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd
第三个参数是须要监听的fd
第四个参数是告诉内核须要监听什么事.
struct_event结构以下:
typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t; struct epoll_event { __uint32_t events;
events能够是如下几个宏的集合:
EPOLLIN:表示对应的文件描述符可读(包括对端SOCKET正常关闭)
EPOLLOUT:表示对应的文件描述符能够写
EPOLLPRI:表示对应的文件描述符有紧急的数据可读
EPOLLERR表示对应的文件描述符发生错误
EPOLLHUP:表示对应的文件描述符被挂断
EPOLLET:将EPOLL设为边缘触发模式(Edge Triggered).这是相对于水平触发(Level Triggered)来讲的
EPOLLONSHOT:只监听一次,当监听玩此次事件以后,若是还须要继续监听这个socket的话,须要再次把这个socket加入到WPOLL队列里
3) int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
参数events用来从内核获得事件的集合,maxevents告以内核这个events有多大,这个maxevents的值不能大于建立epoll_create()时的size,参数timeout是超时时间(毫秒),0会当即返回,-1将不肯定,也有说法是永久阻塞.该函数返回须要处理的事件数目,如返回0表示已超时.
关于ET.LT两种工做模式
ET模式仅当状态发送变化的时候才得到通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说.若是要采用ET模式,须要一直read/write直到出错为止.不少人反映为何采用ET模式只接收了一部分数据就再也得不到通知了,大可能是由于这样.
而LT模式就是只要有数据没有处理就会一直通知下去.
epoll模型
首先经过create_epoll(int maxfds)来建立一个epoll的句柄,其中maxfds为你epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,以后的全部操做将经过这个句柄来进行操做。在用完以后,记得用close()来关闭这个建立出来的epoll句柄。
以后在你的网络主循环里面,每一帧的调用epoll_wait(int epfd, epoll_event events, int max events, int timeout)来查询全部的网络接口,看哪个能够读,哪个能够写了。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd为用epoll_create建立以后的句柄,events是一个epoll_event*的指针,当epoll_wait这个函数操做成功以后,epoll_events里面将储存全部的读写事件。max_events是当前须要监听的全部socket句柄数。最后一个timeout是 epoll_wait的超时,为0的时候表示立刻返回,为-1的时候表示一直等下去,直到有事件范围,为任意正整数的时候表示等这么长的时间,若是一直没有事件,则范围。通常若是网络主循环是单独的线程的话,能够用-1来等,这样能够保证一些效率,若是是和主逻辑在同一个线程的话,则能够用0来保证主循环的效率。
epoll_wait范围以后应该是一个循环,遍利全部的事件。
#include<iostream> #include<sys/socket.h> #include<sys/epoll.h> #include<netinet/in.h> #include<arpa/inet.h> #include<fcntl.h> #include<unistd.h> #include<stdio.h> #include<errno.h> using namespace std; int main(int argc,char *argv[]) { int maxi,listenfd,connfd,sockfd,epfd,nfds; ssize_t n; char line[100]; listenfd = socket(AF_INET,SOCK_STREAM,0);
epoll和select,poll区别
1. 支持一个进程打开大数目的socket描述符(FD)
select最不能忍受的是一个进程所打开的FD是有必定限制的,由FD_SETSIZE设置,默认值是2048。对于那些须要支持的上万链接数目的IM服务器来讲显然太少了。这时候你一是能够选择修改这个宏而后从新编译内核,不过资料也同时指出这样会带来网络效率的降低
2. IO效率不随FD数目增长而线性降低
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过因为网络延时,任一时间只有部分socket是“活跃”的,可是select/poll每次调用都会线性扫描所有的集合,致使效率呈线性降低。可是epoll不存在这个问题,它只会对“活跃”的socket进行操做——这是由于在内核实现中epoll是根据每一个fd上面的callback函数实现的。
epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用必定的方法后,内核才对全部监视的文件描述符进行扫描,而epoll事先经过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用相似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便获得通知。
3. 使用mmap加速内核与用户空间的消息传递
不管是select,poll仍是epoll都须要内核把FD消息通知给用户空间,如何避免没必要要的内存拷贝就很重要,在这点上,epoll是经过内核于用户空间mmap同一块内存实现的
4. 内核微调
这一点其实不算epoll的优势了,而是整个Linux平台的优势.
相关文章
select、poll、epoll使用小结
Linux上可使用不一样的I/O模型,咱们能够经过下图了解经常使用的I/O模型:同步和异步模型,以及阻塞和非阻塞模型,本文主要分析其中的异步阻塞模型。

1、select使用
这个模型中配置的是非阻塞I/O,而后使用阻塞select系统调用来肯定一个I/O描述符什么时候有操做。使用select调用能够为多个描述符提供通知,对于每一个提示符,咱们能够请求描述符的可写,可读以及是否发生错误。异步阻塞I/O的系统流程以下图所示:

使用select经常使用的几个函数以下:
- FD_ZERO(int fd, fd_set* fds)
- FD_SET(int fd, fd_set* fds)
- FD_ISSET(int fd, fd_set* fds)
- FD_CLR(int fd, fd_set* fds)
- int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
fd_set类型能够简单的理解为按bit位标记句柄的队列。具体的置位、验证可使用FD_SET,FD_ISSET等宏实现。在select函数中,readfds、writefds和exceptfds同时做为输入参数和输出参数,若是readfds标记了一个位置,则,select将检测到该标记位可读。timeout为设置的超时时间。
下面咱们来看如何使用select:
- SOCKADDR_IN addrSrv;
- int reuse = 1;
- SOCKET sockSrv,connsock;
- SOCKADDR_IN addrClient;
- pool pool;
- int len=sizeof(SOCKADDR);
- sockSrv=socket(AF_INET,SOCK_STREAM,0);
-
- addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
- addrSrv.sin_family=AF_INET;
- addrSrv.sin_port=htons(port);
-
- if(bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR))<0)
- {
- fprintf(stderr,"Failed to bind");
- return ;
- }
-
- if(listen(sockSrv,5)<0)
- {
- fprintf(stderr,"Failed to listen socket");
- return ;
- }
- setsockopt(sockSrv,SOL_SOCKET,SO_REUSEADDR,(const char*)&reuse,sizeof(reuse));
- init_pool(sockSrv,&pool);
- while(1)
- {
-
- pool.ready_set=pool.read_set;
- pool.nready=select(pool.maxfd+1,&pool.ready_set,NULL,NULL,NULL);
- if(FD_ISSET(sockSrv,&pool.ready_set))
- {
- connsock=accept(sockSrv,(SOCKADDR *)&addrClient,&len);
-
-
- add_client(connsock,&pool);
- }
-
- check_client(&pool);
- }
上面是一个服务器代码的关键部分,设置为异步的模式,而后接受到链接将其添加到链接池中。监听描述符上使用select,接受客户端的链接请求,在check_client函数中,遍历链接池中的描述符,检查是否有事件发生。

2、poll使用
poll函数相似于select,可是其调用形式不一样。poll不是为每一个条件构造一个描述符集,而是构造一个pollfd结构体数组,每一个数组元素指定一个描述符标号及其所关心的条件。定义以下:
- #include <sys/poll.h>
- int poll (struct pollfd *fds, unsigned int nfds, int timeout);
- struct pollfd {
- int fd;
- short events;
- short revents;
- };
每一个结构体的events域是由用户来设置,告诉内核咱们关注的是什么,而revents域是返回时内核设置的,以说明对该描述符发生了什么事件。这点与select不一样,select修改其参数以指示哪个描述符准备好了。在《unix环境高级编程》中有一张events取值的表,以下:
POLLIN :可读除高优级外的数据,不阻塞
POLLRDNORM:可读普通数据,不阻塞
POLLRDBAND:可读O优先数据,不阻塞
POLLPRI:可读高优先数据,不阻塞
POLLOUT :可写普数据,不阻塞
POLLWRNORM:与POLLOUT相同
POLLWRBAND:写非0优先数据,不阻塞
其次revents还有下面取值
POLLERR :已出错
POLLHUP:已挂起,当以描述符被挂起后,就不能再写向该描述符,可是仍能够从该描述符读取到数据。
POLLNVAL:此描述符并不引用一打开文件
对poll函数,nfds表示fds中的元素数,timeout为超时设置,单位为毫秒若为0,表示不等待,为-1表示描述符中一个已经准备好或捕捉到一个信号返回,大于0表示描述符准备好,或超时返回。函数返回值返回值若为0,表示没有事件发生,-1表示错误,并设置errno,大于0表示有几个描述符有事件。
poll的使用和select基本相似。在此再也不介绍。poll相对因而select的优点是监听的描述符数量没有限制。
3、epoll学习
epoll有两种模式,Edge Triggered(简称ET) 和 Level Triggered(简称LT).在采用这两种模式时要注意的是,若是采用ET模式,那么仅当状态发生变化时才会通知,而采用LT模式相似于原来的select/poll操做,只要还有没有处理的事件就会一直通知.
1)epoll数据结构介绍:
- typedef union epoll_data
- {
- void *ptr;
- int fd;
- __uint32_t u32;
- __uint64_t u64;
- } epoll_data_t;
-
- struct epoll_event
- {
- __uint32_t events;
- epoll_data_t data;
- };
常见的事件以下:
EPOLLIN:表示对描述符的能够读
EPOLLOUT:表示对描述符的能够写
EPOLLPRI:表示对描述符的有紧急数据能够读
EPOLLERR:发生错误
EPOLLHUP:挂起
EPOLLET:边缘触发
EPOLLONESHOT:一次性使用,当监听完此次事件以后,若是还须要继续监听这个socket的话,须要再次把这个socket加入到EPOLL队列里
2)函数介绍
epoll的三个函数
- int epoll_creae(int size);
功能:该函数生成一个epoll专用的文件描述符
参数:size为epoll上能关注的最大描述符数
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:用于控制某个epoll文件描述符时间,能够注册、修改、删除
参数:epfd由epoll_create生成的epoll专用描述符
op操做:EPOLL_CTL_ADD 注册 EPOLL_CTL_MOD修改 EPOLL_DEL删除
fd:关联的文件描述符
evnet告诉内核要监听什么事件
- int epoll_wait(int epfd,struct epoll_event*events,int maxevents,int timeout);
功能:该函数等待i/o事件的发生。
参数:epfd要检测的句柄
events:用于回传待处理时间的数组
maxevents:告诉内核这个events有多大,不能超过以前的size
timeout:为超时时间
使用方法参考:https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/epoll-example.c
epoll支持的FD上限是最大能够打开文件的数目(select面临这样的问题),IO效率不随FD数目增长而线性降低(select、poll面临的问题)使用mmap加速内核与用户空间的消息传递。如今libevent封装了几种的实现,能够经过使用libevent来实现多路复用。
本文参考:https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/
http://www.ibm.com/developerworks/cn/linux/l-cn-edntwk/index.html?ca=drs-
http://www.ibm.com/developerworks/cn/linux/l-async/