文章目录
- I/O多路转接-poll
- poll初识
- poll函数
- poll的小测试-监控标准输入
- poll服务器
- poll_server.cc
- poll的优点
- poll的缺点
I/O多路转接-poll
poll初识
poll也是系统提供的一个多路转接接口, poll系统调用也可以让我们的程序同时监视多个文件描述符上的事件是否就绪,和select的定位是一样的,适用场景也是一样的
poll函数
#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
//int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明
- fds:数组起始地址,每一个
struct pollfd
元素包含三部分内容:文件描述符,监视的事件集合,就绪的事件集合 - nfds:表示fds数组的长度
- timeout:表示poll函数的超时时间,单位是毫秒(ms)
返回值说明
- 如果函数调用成功,则返回有事件就绪的文件描述符个数,
- 如果timeout时间耗尽,则返回0,
- 如果函数调用失败,则返回-1,同时错误码会被设置
- poll调用失败时,错误码可能被设置为:
EINVAL
:nfds值超过RLIMIT_NOFILE值,EINTR
:此调用被信号所中断EINVAL
:nfds值超过RLIMIT_NOFILE值,ENOMEM
:核心内存不足,
- poll调用失败时,错误码可能被设置为:
关于
struct pollfd
结构
struct pollfd结构当中包含三个成员:
- fd:表示你要关心哪个文件描述符的事件,若设置为负值则忽略events字段并且revents字段返回0,
- events:需要OS帮你监视该文件描述符fd上的哪些事件 (用户告诉内核所要关心的事件)
- revents:poll函数返回时告知用户该文件描述符fd上的哪些事件已经就绪, (内核告诉用户的就绪事件)
关于events和revents的取值:
事件 | 描述 | 是否可作为输入 | 是否可作为输出 |
---|---|---|---|
POLLIN | 数据(包括普通数据和优先数据)可读 | 是 | 是 |
POLLRDNORM | 普通数据可读 | 是 | 是 |
POLLRDBAND | 优先级带数据可读(Linux不支持) | 是 | 是 |
POLLPRI | 高优先级数据可读,比如TCP带外数据 | 是 | 是 |
POLLOUT | 数据(包括普通数据和优先数据)可写 | 是 | 是 |
POLLWRNORM | 普通数据可写 | 是 | 是 |
POLLWRBAND | 优先级带数据可写 | 是 | 是 |
POLLRDHUP | TCP连接被对方关闭,或者对方关闭了写操作,它由GNU引入 | 是 | 是 |
POLLERR | 错误 | 否 | 是 |
POLLHUP | 挂起。比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件 | 否 | 是 |
POLLNVAL | 文件描述符没有打开 | 否 | 是 |
实际上,这些都是以宏的方式进行定义的,并且这些宏的特点是:它们的二进制序列当中有且只有一个比特位是1,并且比特位为1的位置不会重复,是唯一的
因此在调用poll函数之前,可以通过|
运算符将要监视的事件添加到events成员当中
例子:关心读和写事件是否就绪:
events |= POLLIN; events |= POLLIOUT;
在poll函数返回后,可以通过&
运算符检测revents成员中是否包含特定事件,以得知对应文件描述符的特定事件是否就绪
例子:检测事件是否就绪
//写事件是否就绪
if(revents & POLLIOUT) {}
//读事件是否就绪
if(revents & POLLIOUT) {}
参数timeout的取值:
- -1:poll调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪
- 0:poll调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,poll检测后都会立即返回
- 特定的时间值:poll调用后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后poll进行超时返回
poll的小测试-监控标准输入
我们关心标准输入-0号描述符的读取事件是否就绪, 是因为标准输入有明显的读取事件就绪和不就绪的特征,也就是不给它输入数据就是不就绪,给它输入数据的话,读取事件就是就绪的
#include <iostream>
#include <unistd.h>
#include <poll.h>
int main()
{
struct pollfd rfds;
rfds.fd = 0;//你要关心的文件描述符
rfds.events = POLLIN;
rfds.revents = 0;
//事件循环
while(1)
{
//int n = poll(&rfds, 1, 1000); //每隔1000ms即1s就timeout一次 1s之内有事件就绪就返回,没有事件就绪就timeout
//int n = poll(&rfds, 1, 0); //非阻塞轮询
int n = poll(&rfds, 1, -1); //永久阻塞
switch(n)
{
case 0://超时
std::cout << "time out ..." << std::endl;
break;
case -1://出错
std::cerr << "poll error" << std::endl;
break;
default:
std::cout << "有事件发生..." << std::endl;
//因为当前只关心0号描述符,不需要轮询检测哪个文件描述符的事件发生
if(rfds.revents & POLLIN) //判断读取事件是否发生
{
std::cout << rfds.fd << " 上面的读事件发生了" << std::endl;
char buffer[128];
//从标准输入里读取,读到buffer中,我们认为读取到的是字符串,所以少读一个字节,方便后续在字符串末尾添加\0
ssize_t s = read(0, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = '\0';
std::cout << "有人说# " << buffer << std::endl;
}
}
break;
}
}
return 0;
}
演示:
情况1:当读取事件就绪,但是没有进行读取的时候:
为什么只输入了一次数据,就一直显示有事件发生?
原因就是:数据在键盘输入了,回车也按下去了,但是没有人读取,这个数据在输入缓冲区当中,有事件发生就是:没有人读取,但是数据一直存在,poll就会一直通知你, 如何让他不再通知我? 上层把数据取走
timeout设为1000 :即1s就timeout一次 1s之内有事件就绪就返回,没有事件就绪就timeout
timeout为0:非阻塞轮询
timeout为-1:阻塞等待,有事件就绪了才返回
poll服务器
为了方便后序使用,我们可以对套接字的创建,绑定,监听等进行封装在Sock.hpp文件里面
poll的工作流程和select是基本类似的,这里我们也实现一个简单poll服务器,该服务器也只是读取客户端发来的数据并进行打印
1)首先,我们需要创建套接字,完成绑定,监听的操作
2)poll服务器要做的就是不断调用poll函数,当事件就绪时对应执行某种动作即可,
- 在poll服务器开始死循环调用poll函数之前,需要定义一个数组,该数组当中的每个位置都是一个struct pollfd结构体
- 后续调用poll函数时会作为参数进行传入,先将数组当中每个位置初始化为无效,并将监听套接字添加到数组当中,服务器刚开始运行时只需要监视监听套接字的读事件是否就绪
3)此后,poll服务器就不断调用poll函数监视读事件是否就绪
- 如果poll函数的返回值大于0,则说明poll函数调用成功,此时已经有文件描述符的读事件就绪,接下来就应该对就绪事件进行处理
- 如果poll函数的返回值等于0,则说明timeout时间耗尽,此时直接准备进行下一次poll调用即可
- 如果poll函数的返回值为-1,则说明poll调用失败,此时也让服务器准备进行下一次poll调用
- 但实际应该进一步判断错误码,根据错误码来判断是否应该继续调用poll函数
局部代码:
#include <iostream>
#include <string>
#include <poll.h>
#include "Sock.hpp"
#define NUM 128 //数组长度
#define INVALID_FD -1 //无效fd
struct pollfd pollfds[NUM];
static void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
//我们是后序这样启动服务器的: ./poll_server 端口号
int main(int argc,char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
//创建套接字,绑定,监听
uint16_t port = atoi(argv[1]);
int listen_sock = Sock::Socket();
Sock::Bind(listen_sock,port);
Sock::Listen(listen_sock);
//先将数组的所有位置的文件描述符设为无效
for (int i = 0; i < num; i++)
{
fds[i].fd = INVALID_FD;
fds[i].events = 0;
fds[i].revents = 0;
}
//将监听套接字添加到数组0下标位置中,并关心其读事件
pollfds[0].fd = listen_sock;
pollfds[0].events = POLLIN;//关心listen_sock的读取事件是否就绪
pollfds[0].revents = 0;
for(;;)
{
int n = poll(pollfds, NUM, -1)
switch (n)
{
case 0: //超时
std::cout << "poll timeout" << std::endl;
break;
case -1: //调用失败
std::cerr << "poll failed" << std::endl;
break;
default://有事件就绪了
std::cout << "有fd对应的事件就绪啦!" << std::endl;
break;
}
}
return 0;
}
事件处理
当poll检测到有文件描述符的读事件就绪,内核就会在其对应的struct pollfd结构中的revents成员中添加上读事件就绪(本质是和revents和一个宏进行或运算)并返回,接下来poll服务器就应该对就绪事件进行处理了,事件处理过程如下:
-
首先遍历数组的元素, (struct pollfd结构体), 如果该结构当中的fd有效,且revents当中包含读事件就绪的宏,则说明该文件描述符的读事件就绪,接下来就需要进一步判断该文件描述符是监听套接字还是与客户端建立的套接字
- 如果是监听套接字的读事件就绪:调用accept函数将底层建立好的连接获取上来
-
注意:此时不能直接读取,因为有新链接到来,不代表有数据就绪,不代表能读取,如果此时没有数据,但是我们进行了读取,那么进程就会阻塞在那里进行等待,
-
那么什么时候数据到来呢?不知道 ,可是,谁可以最清楚的知道那些fd上面可以读取了?poll!
-
我们可以将获取到的套接字添加到数组当中,表示下一次调用poll函数时需要监视该套接字的读事件
-
注意:因为这里将数组的大小是固定设置的,因此在将新获取连接对应的文件描述符添加到数组时,可能存在数组已满而添加失败,这时poll服务器只能将刚刚获取上来的连接对应的套接字进行关闭
-
-
如果是与客户端建立的连接对应的文件描述符((普通文件描述符))读事件就绪
- 则调用read函数读取客户端发来的数据,并将读取到的数据在服务器端进行打印
- 此时因为读取事件已经就绪了,所以调用read函数并不会阻塞
- 则调用read函数读取客户端发来的数据,并将读取到的数据在服务器端进行打印
-
如果在调用read函数时发现客户端将连接关闭或read函数调用失败
- 则poll服务器也直接关闭对应的连接
- 并将该连接对应的文件描述符从fds数组当中清除, 下一次调用poll函数时无需再监视该套接字的事件
- 则poll服务器也直接关闭对应的连接
poll_server.cc
#include <iostream>
#include <string>
#include <poll.h>
#include "Sock.hpp"
#define NUM 128 //数组长度
#define INVALID_FD -1 //无效fd
struct pollfd pollfds[NUM];
static void Usage(std::string proc)
{
std::cout << "Usage: " << proc << " port" << std::endl;
}
//我们是后序这样启动服务器的: ./poll_server 端口号
int main(int argc,char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
//创建套接字,绑定,监听
uint16_t port = atoi(argv[1]);
int listen_sock = Sock::Socket();
Sock::Bind(listen_sock,port);
Sock::Listen(listen_sock);
//先将数组的所有位置的文件描述符设为无效
for (int i = 0; i < NUM; i++)
{
pollfds[i].fd = INVALID_FD;
pollfds[i].events = 0;
pollfds[i].revents = 0;
}
//将监听套接字添加到数组0下标位置中,并关心其读事件
pollfds[0].fd = listen_sock;
pollfds[0].events = POLLIN;//关心listen_sock的读取事件是否就绪
pollfds[0].revents = 0;
for(;;)
{
int n = poll(pollfds, NUM, -1);//-1表示阻塞等待
switch (n)
{
case 0: //超时
std::cout << "poll timeout" << std::endl;
break;
case -1: //调用失败
std::cerr << "poll failed" << std::endl;
break;
default://有事件就绪了
std::cout << "有fd对应的事件就绪啦!" << std::endl;
//需要遍历数组,看是哪个文件描述符上的事件就绪了
for(int i = 0;i<NUM;i++)
{
if(pollfds[i].fd == INVALID_FD) //跳过无效的位置
continue;
if(pollfds[i].fd == listen_sock && pollfds[i].revents&POLLIN)//监听套接字上的读取事件就绪了
{
std::cout << "listen_sock: " << listen_sock << " 有了新的链接到来" << std::endl;
int sock = Sock::Accept(listen_sock);
if(sock>=0)
{
std::cout << "listen_sock: " << listen_sock << " 获取新的链接成功" << std::endl;
//此时不能直接进行读取,因为新链接到来,不意味着有数据到来
//所以需要将新获取到的套接字添加到数组中,并关心其读事件是否就绪
//在数组中找一个没有被使用的位置
int pos = 1;
for(;pos<NUM;pos++)
{
if(pollfds[pos].fd == INVALID_FD)
{
break;
}
}
//跳出循环有两种情况:
if(pos < NUM)//case1: 找到了一个位置没有被使用
{
std::cout << "新链接: " << sock << " 已经被添加到了数组[" << pos << "]的位置" << std::endl;
pollfds[pos].fd = sock;
pollfds[pos].events = POLLIN;
}
else//case2: 没有空位置,说明服务器已经满载,没法处理新的请求了
{
std::cout << "服务器已经满载了,关闭新的链接" << std::endl;
close(sock);
}
}
}
else if(pollfds[i].revents &POLLIN) //普通文件描述符上的读事件就绪
{
std::cout << "sock: " << pollfds[i].fd << " 上面有了读事件,可以读取了" << std::endl;
char buffer[1024];
ssize_t s = read(pollfds[i].fd,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s] = '\0';
std::cout << "client[ " << pollfds[i].fd << "]# " << buffer << std::endl;
}
else if(s == 0) //对端关闭了链接
{
std::cout << "sock: " << pollfds[i].fd << "关闭了, client退出啦!" << std::endl;
close(pollfds[i].fd);
std::cout << "已经在数组下标pollfds[" << i << "]"<< "中,去掉了sock: " << pollfds[i].fd<< std::endl;
//连接对应的文件描述符从fds数组当中清除,下一次调用poll函数时无需再监视该套接字的事件
pollfds[i].fd = INVALID_FD;
pollfds[i].events = 0;
pollfds[i].revents = 0;
}
else//读取失败
{
close(pollfds[i].fd);
std::cout << "已经在数组下标pollfds[" << i << "]" << "中,去掉了sock: " << pollfds[i].fd << std::endl;
//连接对应的文件描述符从fds数组当中清除,下一次调用poll函数时无需再监视该套接字的事件
pollfds[i].fd = INVALID_FD;
pollfds[i].events = 0;
pollfds[i].revents = 0;
}
}
}
break;
}
}
return 0;
}
测试
我们编写的poll服务器在调用poll函数时,将timeout的值设置成了-1,因此运行服务器后如果没有客户端发来连接请求,那么服务器就会在调用poll函数后进行阻塞等待
最初poll的数组中只有监听套接字, 当我们用telnet工具连接poll服务器后,poll服务器调用的poll函数在检测到监听套接字的读事件就绪后就会调用accept获取建立好的连接,然后将新获取到的套接字添加到数组中,并关心其读事件是否就绪, 当数据就绪了,就会通知我们,此时客户端发来的数据也能够成功被poll服务器收到并进行打印输出
此外,poll服务器也是一个单进程服务器,但是它也可以同时为多个客户端提供服务
当服务器端检测到客户端退出后,也会关闭对应的连接,并将对应的套接字从数组当中清除
poll的优点
- struct pollfd结构当中包含了events和revents,将fd和事件集合到结构体中,使用更方便
- 相当于将select的输入输出型参数进行分离,避免原始数据被修改, 因此在每次调用poll之前,不需要像select一样重新对参数进行设置,
- 改fd集为pollfd数组,理论上解决了监视fd的个数有上限的问题
- poll可监控的文件描述符数量没有限制
- poll也可以同时等待多个文件描述符,能够提高IO的效率
说明一下:
- 虽然代码中将fds数组的元素个数定义为1024,但fds数组的大小是可以继续增大的,poll函数能够帮你监视多少个文件描述符是由传入poll函数的第二个参数决定的,
- 而fd_set类型只有1024个比特位,因此select函数最多只能监视1024个文件描述符,
poll的缺点
- 和select函数一样,当poll返回后,仍要轮询检测pollfd数组中就绪的事件
- 每次调用poll,都需要把大量的struct pollfd结构从用户态拷贝到内核态,这个开销也会随着poll监视的文件描述符数目的增多而增大,
- 同时每次调用poll都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大,