Linux-高级IO之poll、epoll
@TOC
poll基本知识
前一篇:高级IO之select(点击直达)讲解了select,下面来看一下poll.
poll的机制和select,管理多个描述符也是进行轮询,根据描述符的状态进行处理,区别是poll没有最大文件描述符数量的限制。
poll函数接口
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明:
- fds是一个poll函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合.
- nfds表示fds数组的长度.
- timeout表示poll函数的超时时间, 单位是毫秒(ms)
pollfd结构:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
fd是哪个文件描述符,events:用户告诉内核你要帮我关心哪个文件描述符上的什么事件,revent:内核告诉用户,文件描述符上的事件已经就绪了。
events和revents的取值:
事件 | 描述 | 是否可作为输入 | 是否可作为输出 |
---|---|---|---|
POLLIN | 数据(包括普通数据和优先数据) | 是 | 是 |
POLLRDNORM | 普通数据可读 | 是 | 是 |
POLLRDBAND | 优先数据可读(Linux不支持) | 是 | 是 |
POLLPRI | 高优先级数据可读,比如TCP外带数据 | 是 | 是 |
POLLOUT | 数据(包括普通数据和优先数据) | 是 | 是 |
POLLWRNORM | 普通数据可写 | 是 | 是 |
POLLWRBAND | 优先级带数据可写 | 是 | 是 |
POLLRDHUP | TCP连接被对方关闭,或者对方关闭了写操作 | 是 | 是 |
POLLERR | 错误 | 否 | 是 |
POLLHUP | 挂起 | 否 | 是 |
POLLNVAL | 文件描述符没有打开 | 否 | 是 |
我们常用的就是读和写事件。
返回值
- 返回值小于0, 表示出错;
- 返回值等于0, 表示poll函数等待超时;
- 返回值大于0, 表示poll由于监听的文件描述符就绪而返回
poll的优缺点
poll优点
- pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比
select更方便. - poll并没有最大数量限制 (但是数量过大后性能也是会下降).
poll缺点
poll中监听的文件描述符数目增多时
- 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符.
- 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
- 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.
用poll简单监控标准输入
1 #include <poll.h>
2 #include <unistd.h>
3 #include <iostream>
4 using namespace std;
5 int main() {
6 struct pollfd poll_fd;
7 poll_fd.fd = 0;
8 poll_fd.events = POLLIN;
9
10 for (;;) {
11 int ret = poll(&poll_fd, 1, 1000);
12 if (ret < 0) {
13 cerr<<"poll"<<endl;
14 continue;
15 }
16 if (ret == 0) {
17 cout<<"poll timeout"<<endl;
18 continue;
19 }
20 if (poll_fd.revents == POLLIN) {
21 char buf[1024] = {0};
22 read(0, buf, sizeof(buf) - 1);
23 cout<<buf<<endl;
24 }
25 }
26 }
epoll
它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法.epoll总共有3个接口。
epoll的相关接口
int epoll_create(int size);
参数说明:
- 自从linux2.6.8之后,size参数是被忽略的. 用完之后, 必须调用close()关闭.
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数说明:
- 它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
- 第一个参数是epoll_create()的返回值(epoll的句柄).
- 第二个参数表示动作,用三个宏来表示.
- 第三个参数是需要监听的fd.
- 第四个参数是告诉内核需要监听什么事
第二个参数
EPOLL_CTL_ADD
:注册新的fd到epfd中;EPOLL_CTL_MOD
:修改已经注册的fd的监听事件;EPOLL_CTL_DEL
:从epfd中删除一个fd
struct epoll_event结构:
用 vim /usr/include/sys/epoll.h 看epoll代码
79 typedef union epoll_data
80 {
81 void *ptr;
82 int fd;
83 uint32_t u32;
84 uint64_t u64;
85 } epoll_data_t;
86
87 struct epoll_event
88 {
89 uint32_t events; /* Epoll events */
90 epoll_data_t data; /* User data variable */
91 } __EPOLL_PACKED;
92
可以看到events是一些宏值
- EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
- EPOLLOUT : 表示对应的文件描述符可以写;
- EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
- EPOLLERR : 表示对应的文件描述符发生错误;
- EPOLLHUP : 表示对应的文件描述符被挂断;
- EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
- EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里
epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在epoll监控的事件中已经发送的事件。
参数说明:
- 参数events是分配好的epoll_event结构体数组.
- epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存).
- maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
- 参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞). 如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败
epoll工作原理
- 当进程调用epoll_create时,内核创建一个红黑树,红黑树的结点包含对应的事件,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)
- 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法,这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到就绪队列中
- 在epoll中,对于每一个事件,都会建立一个epitem结构体.
- epoll_ctrl将要监控的文件描述符进行注册;
- epoll_wait, 则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度
是O(1).只需检查就绪队列是否有结点。
epoll优点
- 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
- 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
- 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
- 没有数量限制: 文件描述符数目无上限
select、poll、epoll对比
epoll工作方式
epoll有2种工作方式-水平触发(LT)和边缘触发(ET)
水平触发Level Triggered 工作模式
epoll默认状态下就是LT工作模式.
- 当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
例:只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait仍然会立刻返回并通知socket读事件就绪. - 直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
- 支持阻塞读写和非阻塞读写
边缘触发Edge Triggered工作模式
如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式.
- 当epoll检测到socket上事件就绪时, 必须立刻处理
- 如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候, epoll_wait 不会再返回了
- 也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会
- ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll.
- 只支持非阻塞的读写
select和poll其实也是工作在LT模式下. epoll既可以支持LT, 也可以支持ET
在ET模式下必须要将fd设置位非阻塞
epoll的使用场景
epoll的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll的性能可能适得其反.对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll.
简单的epoll LT服务器
#include "Sock.hpp"
#define SIZE 64
class bucket {
public:
char buffer[20]; //request
int pos;
int fd;
bucket(int sock) :fd(sock), pos(0)
{
memset(buffer, 0, sizeof(buffer));
}
~bucket()
{}
};
class EpollServer {
private:
int lsock;
int port;
int epfd;
public:
EpollServer(int _p = 8081) :port(_p)
{}
void AddEvent2Epoll(int sock, uint32_t event)
{
struct epoll_event ev;
ev.events = event;
if (sock == lsock) {
ev.data.ptr = nullptr;
}
else {
ev.data.ptr = new bucket(sock);
}
epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);
}
void DelEventFromEpoll(int sock)
{
close(sock);
epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr);
}
void InitServer()
{
lsock = Sock::Socket();
Sock::Setsockopt(lsock);
Sock::Bind(lsock, port);
Sock::Listen(lsock);
epfd = epoll_create(256);
if (epfd < 0) {
cerr << "epoll_create error" << endl;
exit(5);
}
cout << "listen sock: " << lsock << endl;
cout << "epoll fd: " << epfd << endl;
}
void HandlerEvents(struct epoll_event revs[], int num)
{
for (int i = 0; i < num; i++) {
uint32_t ev = revs[i].events;
if (ev & EPOLLIN) {
if (revs[i].data.ptr != nullptr) {
bucket* bp = (bucket*)revs[i].data.ptr;
ssize_t s = recv(bp->fd, bp->buffer + bp->pos, sizeof(bp->buffer) - bp->pos, 0);
if (s > 0) {
bp->pos += s;
cout << "client# " << bp->buffer << endl;
if (bp->pos >= sizeof(bp->buffer)) {
struct epoll_event temp;
temp.events = EPOLLOUT;
temp.data.ptr = bp;
epoll_ctl(epfd, EPOLL_CTL_MOD, bp->fd, &temp);
}
}
else if (s == 0)
{
DelEventFromEpoll(bp->fd);
delete bp;
}
else {
}
}
else {
int sock = Sock::Accept(lsock);
if (sock > 0) {
AddEvent2Epoll(sock, EPOLLIN);
}
}
}
else if (ev & EPOLLOUT) {
bucket* bp = (bucket*)revs[i].data.ptr;
send(bp->fd, bp->buffer, sizeof(bp->buffer), 0);
DelEventFromEpoll(bp->fd);
delete bp;
}
else {
}
}
}
void Start()
{
AddEvent2Epoll(lsock, EPOLLIN);
int timeout = -1;
struct epoll_event revs[SIZE];
for (;;) {
int num = epoll_wait(epfd, revs, SIZE, timeout);
switch (num) {
case 0:
cout << "time out ..." << endl;
break;
case -1:
cerr << "epoll_wait error" << endl;
break;
default:
HandlerEvents(revs, num);
break;
}
}
}
~EpollServer()
{
close(lsock);
close(epfd);
}
};
- 点赞
- 收藏
- 关注作者
评论(0)