I/O多路转接之poll函数
如上节所述,poll
函数是另一个支持I/O多路转接的函数。
与select
不同的是poll
函数支持更多的条件,而非select
函数仅有的三种条件(可读、可写以及异常)。
同时poll
函数是以struct pollfd
数组类型保存文件描述符(没有文件描述符的数量限制),并为每个文件描述符指定关注的哪些条件,而select
函数则是另一种角度,为每个条件设置哪些文件描述符。
poll
函数
poll
函数是另一种用来支持非阻塞I/O的方式,可用于任何类型的文件描述符。
#include <poll.h>
int poll(struct pollfd fdarray[], nfds_t nfds, int timeout);
// 返回值:成功返回就绪的文件描述符数目;超时返回0;失败返回-1
与select
不同,poll
函数不是为每个条件(可读、可写或异常条件)都构造一个描述符集,而是构造一个pollfd
结构的数组,每个数组元素指定一个描述符编号以及对该描述符关心的条件。
timeout
函数指定等待的时长,单位/毫秒。
timeout 取值 | 说明 |
---|---|
-1 | 永远等待。直到所指定的文件描述符中至少有一个已经准备好(返回准备好的文件描述符数目),或者捕捉到一个信号(返回-1,并设置errno 为EINTR )时返回。 |
0 | 不等待。测试一遍所有描述符并立即返回。是一种轮询方式,检测所有文件描述符的状态但不阻塞。 |
大于0 | 等待timeout 毫秒。当指定的文件描述符至少一个已准备好(返回准备好的文件描述符数),或者timeout 到期(返回0)或者捕捉到信号时(返回-1,并设置errno )则返回。 |
nfds
参数指定fdarray
数组元素的个数。
fdarray
参数是一个pollfd
数组,该数组的格式如下:
/* Data structure describing a polling request. */
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. */
};
在struct pollfd
结构体中,fd
表示要关注的文件描述符,events
表示要关注的该描述符上的哪些事件,revents
由内核设置,表示该描述符上发生了哪些事件。
events
和revents
的事件取值说明如下:
标志名 | 传入events | 返回revents | 说明 |
---|---|---|---|
POLLIN | ✅ | ✅ | 可以不阻塞读高优先级数据以外的数据,等同于POLLRDNORM | POLLRDBAND |
POLLRDNORM | ✅ | ✅ | 可以不阻塞地普通数据 |
POLLRDBAND | ✅ | ✅ | 可以不阻塞地读优先级数据,通常指TCP地带外数据。 |
POLLPRI | ✅ | ✅ | 可以不阻塞地读高优先级数据,一般指实时数据 |
POLLOUT | ✅ | ✅ | 可以不阻塞地写普通数据 |
POLLWRNORM | ✅ | ✅ | 等同于POLLOUT |
POLLWRBAND | ✅ | ✅ | 可以不阻塞地写优先级数据,一般实时数据的优先级较高 |
POLLERR | ✅ | 已出错 | |
POLLHUP | ✅ | 已挂断(例如TCP远端关闭,管道的写端关闭等) | |
POLLNVAL | ✅ | 描述符没有引用一个打开文件 |
最后三种是由内核在返回时设置,即使未传入events
字段中, 如果相应条件已发生,也会设置到revents
中。
注意:select
和poll
函数在被信号中断之后是不会自动重启的(即使设置了SA_RESTART
标志),而是直接返回-1表示错误,同时设置errno=EINTR
。
与select
的区别
poll
和select
函数都是用来提供I/O多路复用功能的,它们在以下这些方面具有显著差异:
差异点 | select | poll |
---|---|---|
参数形式 | 使用fd_set 类型传递需要关注的文件描述符列表,每个事件都有一个对应的fd_set 参数 | 使用struct pollfd 数组类型来指定需要监听的文件描述符及在其之上的事件 |
描述符数量 | 所支持的最大描述符数量由内核设定,一般最大为1024 | 没有固定上限,受系统资源限制。 |
事件标志 | 支持三种事件,可读、可写以及异常事件 | 支持多种事件,包含可读、可写、异常、连接断开(POLLHUP)、优先级带数据等 |
实现方式 | 每次调用都会将文件描述符集拷贝到内核中,当数量较大时性能一般 | 不需要对文件描述符集合进行操作 |
使用场景 | 文件描述符数量较少,兼容性要求高的场景 | 文件描述符数量多,并且性能要求较高的场景 |
示例
下面的示例中验证poll
函数的基本用法,用来检测标准输入的状态,当标准输入有数据可读时,文件描述状态变为就绪并从poll
函数返回并且此时的返回值为1。
并且在持续5s之内都没有数据可读时会返回0,表示超时。
同时还设置了捕获SIGUSR1
信号,并对该信号设置了中断自动重启,用来验证信号导致poll
中断并且不会自动重启的功能。
具体的示例代码如下:
#include <errno.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/poll.h>
#include <unistd.h>
static void sig_usr1(int signo)
{
printf("caught signo\n");
}
int main()
{
// 1. 创建pollfd数组,存储关心的文件描述符个数以及事件
struct pollfd fdarray[1];
// 设置关注的文件描述符以及事件
fdarray[0].fd = STDIN_FILENO;
fdarray[0].events = POLLIN | POLLPRI;
// 表示fdarray的元素个数
int nfds = 1;
// 表示等待5000ms,超时到期之前没有收到信号中断或者准备就绪的文件描述,则返回0
int timeout = 5000;
// 2. 捕捉信号
struct sigaction newact, oldact;
sigemptyset(&newact.sa_mask);
// 设置信号中断自动重启,正常来说应该是不生效,因为poll函数不支持自动重启
newact.sa_flags |= SA_RESTART;
newact.sa_handler = sig_usr1;
if (sigaction(SIGUSR1, &newact, &oldact) < 0)
{
perror("sigaction");
exit(-1);
}
while (1)
{
// poll函数不会修改传入的events以及timeout,因此这里不用重新设置
// 重置返回值,防止影响下次调用
for (int i = 0; i < nfds; i++)
fdarray[i].revents = 0;
int retval = poll(fdarray, nfds, timeout);
if (retval == -1)
{
// 表示错误,原因可能是收到信号或其他错误
if (errno == EINTR)
{
// 由信号导致的错误
printf("interrupt because of signal and exit\n");
exit(-1);
}
else
{
perror("poll");
exit(-1);
}
}
else if (retval == 0)
{
// 表示超时
// 超时到期前没有文件描述符准备就绪
// 也没有捕捉到信号而中断
printf("timeout and continue\n");
}
else
{
printf("ready\n");
// 表示文件描述符准备就绪
for (int i = 0; i < nfds; i++)
{
if (fdarray[i].revents & POLLIN)
{
printf("POLLIN\n");
// 下面这种方式是不正确的,虽然POLLIN等效于POLLRDNORM|POLLRDBAND,
// 但不等同于可以直接检测,具体原因可以查看这些常量的值
// // 有普通数据可读
// if (fdarray[i].revents & POLLRDNORM)
// {
// printf("POLLRDNORM\n");
// char buf[256] = {0};
// read(fdarray[i].fd, buf, sizeof(buf));
// printf("read normal data: %s\n", buf);
// exit(0);
// }
//
// // 有优先级数据可读
// if (fdarray[i].revents & POLLRDBAND)
// {
// char buf[256] = {0};
// read(fdarray[i].fd, buf, sizeof(buf));
// printf("read band data: %s\n", buf);
// exit(0);
// }
char buf[256] = {0};
read(fdarray[i].fd, buf, sizeof(buf));
printf("read normal data: %s\n", buf);
exit(-1);
}
}
}
}
}
执行结果如下:
# 第一次运行
$ ./poll_demo
timeout and continue
11111
ready
POLLIN
read normal data: 11111
# 第二次运行
$ ./poll_demo
timeout and continue
timeout and continue
timeout and continue
caught signo
interrupt because of signal and exit
# 令起一个终端并执行下面命令
$ ps -ef | grep poll_demo
blduan 3251 1176 0 04:49 pts/2 00:00:00 ./poll_demo
blduan 3253 2276 0 04:49 pts/4 00:00:00 grep --color=auto poll_demo
$ kill -10 3251 # 向poll_demo进程发送SIGUSR1信号
从结果上看,设置的对SIGUSR1
信号的中断自动重启功能并没有对poll
函数生效,即poll
函数在收到信号之后仍旧会出错返回并设置errno
。
- 原文作者:生如夏花
- 原文链接:https://blduan.top/post/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/apue/io%E5%A4%9A%E8%B7%AF%E8%BD%AC%E6%8E%A5%E4%B9%8Bpoll%E5%87%BD%E6%95%B0/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。