poll
相比于select函数
,poll的跨平台移植性不如select,因为poll函数只能在linux环境中使用,也是采用轮询
遍历的方式。
与select函数的不同之处:
- poll不会限制文件描述符的个数
- 文件描述符对应一个事件结构,这个结构中有两个事件,一个是要监控的文件描述符,另一个是这个文件描述符所对应的事件
函数接口
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- fds:事件结构数组
- nfds:fds数组中有效的元素个数
- timeout:超时时间
struct pollfd 结构体
struct pollfd
{
int fd; /* File descriptor to poll. */
short int events; /* Types of events poller cares about. */
short int revents; /* Types of events that actually occurred. */
};
其中:
- fd:该结构体所关心的文件描述符数值
- events:所关心这个文件描述符的那些事件,事件与事件之间采用按位或的方式连接
- revents:当关心的文件描述符产生对应的关心的事件时,返回给调用者发生的事件(每次监控的时候,就会被初始化为空)
events 和 revents的取值 :
事件 | 描述 | 是否可以作为输入 | 是否可以作为输出 |
---|---|---|---|
POLLIN | 普通或优先级带数据可读 | 是 | 是 |
POLLRDNORM | 普通数据可读 | 是 | 是 |
POLLRDBAND | 优先级带数据可读(linux不支持) | 是 | 是 |
POLLPRI | 高优先级数据可读,比如TCP带外数据 | 是 | 是 |
POLLOUT | 数据可写(普通数据和优先数据) | 是 | 是 |
POLLWRNORM | 普通数据可写 | 是 | 是 |
POLLWRBAND | 优先级带数据可写 | 是 | 是 |
POLLERR | 发生错误 | 否 | 是 |
POLLHUP | 发生挂起,管道的写端关闭后,读端描述符将受到POLLHUP事件 | 否 | 是 |
POLLNVAL | 描述字不是一个打开的文件 | 否 | 是 |
编程实例
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <poll.h>
using namespace std;
int main()
{
//创建一个struct pollfd结构体,关心0号文件描述符,标准输入
//监视可读事件
struct pollfd poll_fd;
poll_fd.fd = 0;
poll_fd.events = POLLIN;
//轮询遍历
while(1)
{
int ret = poll(&poll_fd,1,1000);
if(ret < 0)//poll出错
{
perror("poll");
sleep(3);
continue;
}
else if(ret == 0)//监控超时
{
cout<<"poll timeout"<<endl;
continue;
}
else if(poll_fd.revents == POLLIN)//当发送的事件时可读事件时
{
//读取数据
char buf[1024] = {'\0'};
read(0,buf,sizeof(buf) - 1);
cout<<"input : "<<buf<<endl;
}
}
return 0;
}
如果我们一次所传入的数据超过了一次可以接受的数据大小时,就会别分批进行接收
poll 的优缺点:
优点:
- poll采用了
事件结构
的方式,简化了程序,并且他不限制文件描述符的个数
- 不需要我们重新添加文件描述符到事件的结构数组当中
缺点:
- poll也是需要
轮询遍历
事件结构数组,那么当文件描述符增多的时候,性能可能不会那么好了 - poll
不支持跨平台
操作 - poll不会告诉我们哪一个文件描述符就绪了,想要找到这个就绪的文件描述符,就需要我们进行遍历操作
- poll在操作的过程中,也是需要时间将结构数组从用户空间拷贝到内核空间中,再将内核空间拷贝到用户空间
epoll
在man
手册中,对于epoll的描述是:
The epoll API performs a similar task to poll(2): monitoring multiple file descriptors to see if
I/O is possible on any of them.
也就是:
epoll API执行与poll(2)类似的任务:监视多个文件描述符,看看是否存在
I/O是可能的任何一个
epoll是为了处理大量的文件描述符而改进的poll函数
函数接口
创建epoll操作句柄:
#include <sys/epoll.h>
int epoll_create(int size);
- size:定义的是epoll中最大可以监控的文件描述符个数,但是在
linux2.6.8
之后,size参数是被忽略的,变成了可以扩容的方式。(size 不可以传入一个负数)
在使用完之后,必须使用close
函数进行关闭
epoll 的操作:
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd,
struct epoll_event *event);
- epfd:之前使用
epoll_create
函数返回的epoll操作句柄 - op:想让epoll_stl函数所做的哪些事
- fd:告诉epoll函数,我们所关心的文件描述符
- event:他是一个
struct epoll_event
类型的结构体,表示的是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 events */
epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;
struct epoll_event :
-
第一个参数:
uint32_t events
,是我们想让文件描述符关心的事件集合
其中,EPOLLIN,是可读事件;EPOLLOUT,是可写事件 -
第二个参数
epoll_data_t data
,他是一个epoll_data
类型的联合结构体
typedef union epoll_data :
- void* ptr:这里可以传递的信息,就是当epoll监控的文件描述符就绪的时候,等到函数返回,我们可以通过ptr拿到这些信息;其在使用的时候,传入一个结构体,
(自定义结构体)struct my_epoll_data{int fd}
,必须在结构体中包含文件描述符 - int fd:我们所关心的文件描述符,可以当做文件描述符中的事件就绪之后,返回给我们时查看;其取值为文件描述符的数值
- ptr和fd,两者在使用的时候,只能选其中的一个。
监控:
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
- epfd:epoll操作句柄
- events:epoll事件结构数组。作为出参,他返回的是就绪的事件结构(每一个事件结构都对应一个文件描述符)
- maxevents:最大可以拷贝的事件结构数量
- timeout:超时时间
epoll 的工作原理:
- 当某一进程调用epoll_create方法的时候,linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll使用方式相关,分别是红黑树和双向链表
- 那些没有就绪的事件,用于存放通过
epoll_ctl
方法向epoll对象
中添加进来的事件,都会被挂载在红黑树中(这样就可以识别出重复的事件,并且还让查找效率提高) - 当红黑树中某个事件就绪后,就会被拷贝到双向链表中(这是一个内核行为)。所以说,双向链表中的数据都是就绪的
- 当调用epoll_wait函数的时候,双向链表向参数中进行拷贝的方式,是通过改变页表,直接指向物理内存中的双向链表中就绪事件所占用的物理内存
epoll的两种工作方式
水平触发 LT模式
在这种模式下,当epoll
中检测到了等待触发的事件就绪后,可以不立即进行处理,而是只处理一部分。等到第二次调用epoll_wait
函数的时候,可以接着操作刚才没有处理完的数据。他支持阻塞读写与非阻塞读写
这是epoll
函数的默认工作方式,另外select 和 poll
都是水平触发
-
对于可读事件,只要接收缓冲区中的存在数据,就会一直触发该接收该可读事件,直到没有数据可读
-
对于可写事件,只要发送缓冲区中存在数据,就会一直触发该可写事件就绪,直到发送缓冲区中没有数据
函数模拟
/*================================================================
* Copyright (C) 2020 Sangfor Ltd. All rights reserved.
*
* 文件名称:test.cpp
* 创 建 者:dcl
* 创建日期:2020年06月20日
* 描 述:
*
================================================================*/
#include <unistd.h>
#include <cstdio>
#include <sys/epoll.h>
#include <iostream>
using namespace std;
int main()
{
int epollfd = epoll_create(10);
if(epollfd < 0)
{
perror("epoll_create");
return 0;
}
struct epoll_event ev;
ev.events = EPOLLIN;//可读事件
ev.data.fd = 0;
//int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//添加一个文件描述符到红黑树中
int ret = epoll_ctl(epollfd,EPOLL_CTL_ADD,0,&ev);
if(ret < 0)
{
perror("epoll_ctl");
return 0;
}
while(1)
{
struct epoll_event fd_arr[10];
//int epoll_wait(int epfd, struct epoll_event *events,
// int maxevents, int timeout);
int ret = epoll_wait(epollfd,fd_arr,sizeof(fd_arr) / sizeof(fd_arr[0]),3000);
if(ret < 0)
{
perror("epoll_wait");
continue;
}
else if(ret == 0)
{
cout<<"timeout"<<endl;
sleep(1);
continue;
}
cout<<"---"<<ret<<endl;
//文件描述符被触发
for(int i = 0; i < ret; i++)
{
if(fd_arr[i].data.fd == 0)
{
char buf[3] = {'\0'};
read(fd_arr[i].data.fd,buf,sizeof(buf) - 1);
cout<<"input : "<<buf<<endl;
}
}
}
return 0;
}
在设置接收数据的大小的时候,我们给数组设置的比较小,当我们发送大量的数据,就会出现下面这种情况
边缘触发 ET模式
在ET模式下,如果我们一次只是处理了部分数据,那么剩下的数据在下一次触发的时候才会被处理的。
也就是说,ET模式下,当文件描述符就绪后,就只有一次处理文件中数据的机会,所以我们需要使用循环的方式进行接收数据。他支持非阻塞读写
程序模拟:
/*================================================================
* Copyright (C) 2020 Sangfor Ltd. All rights reserved.
*
* 文件名称:test.cpp
* 创 建 者:dcl
* 创建日期:2020年06月20日
* 描 述:
*
================================================================*/
#include <errno.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <cstdio>
#include <iostream>
#include <string>
using namespace std;
inline void slove(struct epoll_event* fd_arr,int ret)
{
for(int i = 0; i < ret; i++)
if(fd_arr[i].data.fd == 0)
{
string ans = "";
while(1)
{
char buf[3] = {'\0'};
ssize_t readsize = read(0,buf,sizeof(buf) - 1);
if(readsize < 0)
{
//说明数据读完了
if(errno == EAGAIN || errno == EWOULDBLOCK)
break;
perror("read");
break;
}
ans += buf;
break;
if(readsize < (ssize_t)sizeof(buf) - 1)
break; //数据读完了
}
if(ans != "") cout<<"read data: "<<ans<<endl;
}
}
int main()
{
//设置文件描述符属性为非阻塞
//int fcntl(int fd, int cmd, ... /* arg */ );
int flag = fcntl(0,F_GETFL);
fcntl(0,F_SETFL,flag | O_NONBLOCK);
//创建epoll操作句柄
int epoll_fd = epoll_create(10);
if(epoll_fd < 0)
{
perror("epoll_create");
return 0;
}
//添加文件描述符
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = 0;
int ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,0,&ev);
if(ret < 0)
{
perror("epoll_ctl");
return 0;
}
while(1)
{
struct epoll_event fd_arr[10];
ret = epoll_wait(epoll_fd,fd_arr,sizeof(fd_arr) / sizeof(fd_arr[0]),-1);
if(ret < 0)
{
perror("epoll_wait");
continue;
}
slove(fd_arr,ret);
}
return 0;
}
没有循环接收数据时的情况
数据的接收过程:
在循环接收的过程中,当某一次我们接收的数据的大小小于我们的最大接收能力的时候,就说明数据读取完了
还有一种意外情况,就是当我们所发送的数据正好是我们最大接收能力的整数倍时,比如说:我们发送一个 123\r\n
,我们一次只能接收两个数据,那么当最后一次接收到数据的时候,其实数据读取完毕了,但是read函数还是处于一个等待数据的阶段,就会卡死,造成一个饥饿的情况
- ET模式,在加上循环后很容易造成饥饿状态
解决方法:将文件描述符设置为非阻塞状态,我们可以调用fcntl函数
int fcntl(int fd, int cmd, ... /* arg */ );
- fd:所要操作的哪一个文件描述符
- cmd:fcntl函数需要做的事情
F_GETFL
,获取文件描述符属性,文件描述符通过返回值返回给我们,arg不需要设置
F_SETFD
,设置文件描述符属性,arg就是文件描述符需要设置的属性,类型为int