I/O复用二:poll

函数原型
#include <poll.h>

int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);

若有描述符就绪,则返回其数目,若超时则为0
若出错则为-1,并设置errno为下列值之一:
    EBADF 一个或多个结构体中指定的文件描述符无效。
    EFAULTfds  指针指向的地址超出进程的地址空间。
    EINTR  请求的事件之前产生一个信号,调用可以重新发起。
    EINVALnfds  参数超出 PLIMIT_NOFILE 值。
    ENOMEM  可用内存不足,无法完成请求。
  • 第一个参数
    struct pollfd {
        int fd;         // 文件描述符
        short events;   // 等待发生的事件
        short revents;  // 实际发生了的事件
    }
    
    • pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。
    • events是监视该文件描述符的事件掩码,由用户来设置这个域。
      #define    POLLIN    0x0001    
      #define    POLLPRI   0x0002   
      #define    POLLOUT   0x0004   
      
    • revents是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。
      这点 和 select 不同,poll 每次检测之后的结果不会修改原来的传入值,而是将结果保留在 revents 字段中,这样就不需要每次检测完都得重置待检测的描述字和感兴趣的事件。
  • 第二个参数
    数组元素个数

  • 第三个参数

    timeout参数指定等待的毫秒数,无论I/O是否准备好,poll()都会返回。

    • 负数值表示无限超时,使poll()一直挂起直到一个指定事件发生。
    • 0指示poll()调用立即返回并列出准备好的I/O的文件描述符,但并不等待其它的事件。
合法事件
  • 输入

    常值 可以作为events输入 可以作为revents结果 说明
    POLLIN ● ● 普通或优先级带数据可读
    POLLRDNORM ● ● 普通数据可读
    POLLRDBAND ● ● 优先级带数据可读
    POLLPRI ● ● 高优先级数据可读
  • 输出

    常值 可以作为events输入 作为revents结果 说明
    POLLOUT ● ● 普通数据可写
    POLLWRNORM ● ● 普通数据可写
    POLLWRBAND ● ● 优先级带数据可写
  • 错误

    常值 可以作为events输入 作为revents结果 说明
    POLLERR ● 发生错误
    POLLHUP ● 发生挂起
    POLLNVAL ● 描述符不是一个打开的文件
  • poll识别三种数据类型:普通, 优先级带,高优先级

    • 所有正规的TCP数据和所有UDP数据被认为是普通数据
    • 读半部关闭(收到对端的FIN)时,也是普通数据,随后读操作返回0
    • 监听套接字上有新的连接既可以认为是普通数据也可以认为是优先级数据
    • TCP连接存在错误既可以认为是普通数据,也可以认为是优先级数据
    • TCP带外数据被认为是优先级带数据
    • 非阻塞式connect的完成被认为是相应套接字可写

    poll函数中参数pollfd结构包含了要监视的 event 和发生的 event,不再使用 select"参数-值"传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

代码示例

回射服务器采用 poll 实现:

#define INIT_SIZE 128

int main(int argc, char **argv) 
{
    int listen_fd, connected_fd;
    int ready_number;
    ssize_t n;
    char buf[MAXLINE];
    struct sockaddr_in client_addr;

    listen_fd = tcp_server_listen(SERV_PORT);

    // 初始化pollfd数组,这个数组的第一个元素是listen_fd,其余的用来记录将要连接的connect_fd
    struct pollfd event_set[INIT_SIZE];
    event_set[0].fd = listen_fd;
    event_set[0].events = POLLRDNORM;

    // 用-1表示这个数组位置还没有被占用
    int i;
    for (i = 1; i < INIT_SIZE; i++) 
    {
        event_set[i].fd = -1;
    }

    for ( ; ; ) 
    {
        if ((ready_number = poll(event_set, INIT_SIZE, -1)) < 0) {
            error(1, errno, "poll failed ");
        }

        if (event_set[0].revents & POLLRDNORM) {
            socklen_t client_len = sizeof(client_addr);
            connected_fd = accept(listen_fd, (struct sockaddr *) &client_addr, &client_len);

            //找到一个可以记录该连接套接字的位置
            for (i = 1; i < INIT_SIZE; i++) {
                if (event_set[i].fd < 0) {
                    event_set[i].fd = connected_fd;
                    event_set[i].events = POLLRDNORM;
                    break;
                }
            }

            if (i == INIT_SIZE) {
                error(1, errno, "can not hold so many clients");
            }

            if (--ready_number <= 0)
                continue;
        }

        for (i = 1; i < INIT_SIZE; i++) {
            int socket_fd;
            if ((socket_fd = event_set[i].fd) < 0)
                continue;
            if (event_set[i].revents & (POLLRDNORM | POLLERR)) {
                if ((n = read(socket_fd, buf, MAXLINE)) > 0) {
                    if (write(socket_fd, buf, n) < 0) {
                        error(1, errno, "write error");
                    }
                } else if (n == 0 || errno == ECONNRESET) {
                    close(socket_fd);
                    event_set[i].fd = -1;
                } else {
                    error(1, errno, "read error");
                }

                if (--ready_number <= 0)
                    break;
            }
        }
    }
}
select 和 poll比较

select()和poll()函数本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理。

poll()没有最大文件描述符数量的限制。

select()返回后,之前没有准备好的文件描述符会从集合当中删除,这样如果下次需要再次添加所有文件描述符或者使用两个相同的文件描述符集合,一个用于备份,一个用于监听,比较复杂。poll不需要这个复杂的操作。

poll()和select()同样存在一个缺点就是包含大量文件描述符的数组被整体复制于用户和内核的地址空间之间,而无论这些文件描述符是否就绪。它的开销随着文件描述符数量的增加而线性增加。


参考资料
[1]《UNIX 网络编程》3th [美] W.Richard Stevens,Bill Fenner,Andrew M. Rudoff
[2] http://www.cnblogs.com/Anker/archive/2013/08/15/3261006.html

最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 13,136评论 6赞 13
  • 本文摘抄自linux基础编程 IO概念 Linux的内核将所有外部设备都可以看做一个文件来操作。那么我们对与外部设...
    lintong阅读 1,713评论 0赞 4
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 137,229评论 19赞 139
  • 我们都不认识彼此,是人生中注定的陌路。每当我们在生命的道路见面,我总会转身想要离去,可惜,你给了我绝望,却抓住了我...
    梦熙珏璃阅读 300评论 0赞 0
  • 在阅读了《琵琶记》这个文学作品之后,我很多的感触,也很是同情蔡伯喈他们一家人的悲惨遭遇。在整个作品中其主要讲的是蔡...
    青春随笔阅读 1,869评论 0赞 1

友情链接更多精彩内容