如上节所述,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,并设置errnoEINTR)时返回。
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由内核设置,表示该描述符上发生了哪些事件

eventsrevents的事件取值说明如下:

标志名传入events返回revents说明
POLLIN可以不阻塞读高优先级数据以外的数据,等同于POLLRDNORM | POLLRDBAND
POLLRDNORM可以不阻塞地普通数据
POLLRDBAND可以不阻塞地读优先级数据,通常指TCP地带外数据。
POLLPRI可以不阻塞地读高优先级数据,一般指实时数据
POLLOUT可以不阻塞地写普通数据
POLLWRNORM等同于POLLOUT
POLLWRBAND可以不阻塞地写优先级数据,一般实时数据的优先级较高
POLLERR已出错
POLLHUP已挂断(例如TCP远端关闭,管道的写端关闭等)
POLLNVAL描述符没有引用一个打开文件

最后三种是由内核在返回时设置,即使未传入events字段中, 如果相应条件已发生,也会设置到revents

注意:selectpoll函数在被信号中断之后是不会自动重启的(即使设置了SA_RESTART标志),而是直接返回-1表示错误,同时设置errno=EINTR

select的区别

pollselect函数都是用来提供I/O多路复用功能的,它们在以下这些方面具有显著差异:

差异点selectpoll
参数形式使用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