原理

和select类似,只是描述fd集合的方式不同,poll使用pollfd结构而非select的fd_set结构。 管理多个描述符也是进行轮询,根据描述符的状态进行处理,但poll没有最大文件描述符数量的限制。

select 和poll的区别

select采用的是位掩码的模型,参考NDK中sysroot/usr/include/sys/select.h,即

#define FD_SETSIZE 1024
#define NFDBITS (8 * sizeof(unsigned long))
#define __FDSET_LONGS (FD_SETSIZE/NFDBITS) typedef struct {
unsigned long fds_bits[__FDSET_LONGS];
} fd_set; #define __FDELT(fd) ((fd) / NFDBITS)
#define __FDMASK(fd) (1UL << ((fd) % NFDBITS))
#define __FDS_BITS(set) (((fd_set*)(set))->fds_bits) #define FD_ZERO(set) (memset(set, 0, sizeof(*(fd_set*)(set)))) #if defined(__BIONIC_FORTIFY)
extern void __FD_CLR_chk(int, fd_set*, size_t);
extern void __FD_SET_chk(int, fd_set*, size_t);
extern int __FD_ISSET_chk(int, fd_set*, size_t);
#define FD_CLR(fd, set) __FD_CLR_chk(fd, set, __bos(set))
#define FD_SET(fd, set) __FD_SET_chk(fd, set, __bos(set))
#define FD_ISSET(fd, set) __FD_ISSET_chk(fd, set, __bos(set))
#else
#define FD_CLR(fd, set) (__FDS_BITS(set)[__FDELT(fd)] &= ~__FDMASK(fd))
#define FD_SET(fd, set) (__FDS_BITS(set)[__FDELT(fd)] |= __FDMASK(fd))
#define FD_ISSET(fd, set) ((__FDS_BITS(set)[__FDELT(fd)] & __FDMASK(fd)) != 0)
#endif /* defined(__BIONIC_FORTIFY) */

不考虑__BIONIC_FORTIFY,即select是通过申请了一个unsigned long fds_bits[]的数组,数组中的每一位代表一个文件句柄的掩码,即select最大只支持fd < 1024的情况,如果fd >= 1024,就必须重新编译内核了。

那么区别点:

  • 区别1:select使用的是定长数组,而poll是通过用户自定义数组长度的形式(pollfd[])。
  • 区别2:select只支持最大fd < 1024,如果单个进程的文件句柄数超过1024,select就不能用了。poll在接口上无限制,考虑到每次都要拷贝到内核,一般文件句柄多的情况下建议用epoll。
  • 区别3:select由于使用的是位运算,所以select需要分别设置read/write/error fds的掩码。而poll是通过设置数据结构中fd和event参数来实现read/write,比如读为POLLIN,写为POLLOUT,出错为POLLERR:
struct pollfd pfd;
pfd.fd = fd;
pfd.events = POLLIN;
pfd.revents = 0;
  • 区别4:select中fd_set是被内核和用户共同修改的,所以要么每次FD_CLR再FD_SET,要么备份一份memcpy进去。而poll中用户修改的是events,系统修改的是revents。所以参考muduo的代码,都不需要自己去清除revents,从而使得代码更加简洁。
  • 区别5:select的timeout使用的是struct timeval *timeout,poll的timeout单位是int。
  • 区别6:select使用的是绝对时间,poll使用的是相对时间。
  • 区别7:select的精度是微秒(timeval的分度),poll的精度是毫秒。
  • 区别8:select的timeout为NULL时表示无限等待,否则是指定的超时目标时间;poll的timeout为-1表示无限等待。所以有用select来实现usleep的。
  • 区别9:理论上poll可以监听更多的事件,比如sysroot/usr/include/asm-generic/poll.h中有
#define POLLIN 0x0001
#define POLLPRI 0x0002
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
#define POLLOUT 0x0004
#define POLLERR 0x0008
#define POLLHUP 0x0010
#define POLLNVAL 0x0020
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
#define POLLRDNORM 0x0040
#define POLLRDBAND 0x0080
#ifndef POLLWRNORM
#define POLLWRNORM 0x0100
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
#endif
#ifndef POLLWRBAND
#define POLLWRBAND 0x0200
#endif
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
#ifndef POLLMSG
#define POLLMSG 0x0400
#endif
#ifndef POLLREMOVE
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
#define POLLREMOVE 0x1000
#endif
#ifndef POLLRDHUP
#define POLLRDHUP 0x2000
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
#endif
#define POLLFREE 0x4000
#define POLL_BUSY_LOOP 0x8000
struct pollfd {
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
int fd;
short events;
short revents;
};

API

poll函数原型

int poll(struct pollfd *fds, unsigned long nfds, int timeout);  
返回值:若有就绪描述符则为其数目,若超时则为0,若出错则为-1

第一个参数是一个 pollfd 的数组

其中 pollfd 的结构如下:

struct pollfd {
int fd; /* file descriptor */
short events; /* events to look for */
short revents; /* events returned */
};

首先是描述符 fd,然后是描述符上待检测的事件类型 events,注意这里的 events 可以表示多个不同的事件,具体的实现可以通过使用二进制掩码位操作来完成,例如,POLLIN 和 POLLOUT 可以表示读和写事件。

events类型的事件可以分为两大类

  • 第一类是可读事件,有以下几种
#define POLLIN     0x0001    /* any readable data available */
#define POLLPRI 0x0002 /* OOB/Urgent readable data */
#define POLLRDNORM 0x0040 /* non-OOB/URG data available */
#define POLLRDBAND 0x0080 /* OOB/Urgent readable data */
  • 第二类是可写事件,有以下几种:
#define POLLOUT    0x0004    /* file descriptor is writeable */
#define POLLWRNORM POLLOUT /* no write type differentiation */
#define POLLWRBAND 0x0100 /* OOB/Urgent data can be written */

一般我们在程序里面统一使用 POLLOUT。套接字可写事件和 select 的 writeset 基本一致,是系统内核通知套接字缓冲区已准备好,通过 write 函数执行写操作不会被阻塞。

其次参数 nfds 描述的是数组 fds 的大小,简单说,就是向 poll 申请的事件检测的个数。

最后一个参数timeout,描述poll的行为

如果是一个 <0 的数,表示在有事件发生之前永远等待;如果是 0,表示不阻塞进程,立即返回;如果是一个 >0 的数,表示 poll 调用方等待指定的毫秒数后返回。

关于返回值,,当有错误发生时,poll 函数的返回值为 -1;如果在指定的时间到达之前没有任何事件发生,则返回 0,否则就返回检测到的事件个数,也就是“returned events”中非 0 的描述符个数。

实践

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/poll.h>
#include <sys/socket.h>
#include <netinet/in.h> #define SERV_PORT 43211
#define LISTENQ 1024
#define INIT_SIZE 128
#define MAXLINE 1024 int tcp_server_listen(int port)
{
int listen_fd;
listen_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); int on = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); int rt1 = bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if(rt1 < 0)
{
perror("bind failed");
return -1;
} int rt2 = listen(listen_fd, LISTENQ);
if(rt2 < 0)
{
perror("listen failed");
return -1;
}
signal(SIGPIPE, SIG_IGN); return listen_fd; } 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
//将监听套接字 listen_fd 和对应的 POLLRDNORM 事件加入到 event_set 里,表示我们期望系统内核检测监听套接字上的连接建立完成事件。
struct pollfd event_set[INIT_SIZE];
event_set[0].fd = listen_fd;
event_set[0].events = POLLRDNORM; //用-1表示这个数组位置还没有被占用
//对应 pollfd 里的文件描述字 fd 为负数,poll 函数将会忽略这个 pollfd
int i;
for(int i = 1; i < INIT_SIZE; i++)
{
event_set[i].fd = -1;
} for(;;)
{
//调用 poll 函数来进行事件检测。poll 函数传入的参数为 event_set 数组,数组大小 INIT_SIZE 和 -1
//之所以传入 INIT_SIZE,是因为 poll 函数已经能保证可以自动忽略 fd 为 -1 的 pollfd,否则我们每次都需要计算一下 event_size 里真正需要被检测的元素大小;
//timeout 设置为 -1,表示在 I/O 事件发生之前 poll 调用一直阻塞。
if((ready_number = poll(event_set, INIT_SIZE, -1)) < 0)
{
perror("poll failed");
return -1;
}
// 使用了如 event_set[0].revent 来和对应的事件类型进行位与操作
// 因为 event 都是通过二进制位来进行记录的,位与操作是和对应的二进制位进行操作,一个文件描述字是可以对应到多个事件类型的。
if(event_set[0].revents & POLLRDNORM)
{
socklen_t client_len = sizeof(client_addr);
//调用 accept 函数获取了连接描述字
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;
}
}
//event_set 已经被很多连接充满了,没有办法接收更多的连接了
if( i == INIT_SIZE)
{
perror("can not hold so many clients");
return -1;
}
//加速优化能力,因为 poll 返回的一个整数,说明了这次 I/O 事件描述符的个数,
//如果处理完监听套接字之后,就已经完成了这次 I/O 复用所要处理的事情,那么我们就可以跳过后面的处理,再次进入 poll 调用。
if(--ready_number <= 0)
continue;
}
//循环处理是查看 event_set 里面其他的事件,也就是已连接套接字的可读事件
for(i = 1; i < INIT_SIZE; i++)
{
int socket_fd;
if((socket_fd = event_set[i].fd) < 0)
continue;
//通过检测 revents 的事件类型是 POLLRDNORM 或者 POLLERR,我们可以进行读操作。
if(event_set[i].revents & (POLLRDNORM | POLLERR))
{
//读取数据正常之后,再通过 write 操作回显给客户端;
if((n = read(socket_fd, buf, MAXLINE)) > 0)
{
if(write(socket_fd, buf, n) < 0)
{
perror("write error");
return -1;
}
}
else if(n == 0 || errno == ECONNRESET)
{
//如果读到 EOF 或者是连接重置,则关闭这个连接,并且把 event_set 对应的 pollfd 重置
close(socket_fd);
event_set[i].fd = -1;
}
else
{
perror("read error");
return -1;
}
if(--ready_number <= 0)
break;
}
}
}
}

运行结果

通过多个telnet客户端登录,互补影响

网络编程:poll的更多相关文章

  1. UNIX网络编程-Poll模型学习

    1.相关接口介绍 1.1 poll ---------------------------------------------------------------------- #include &l ...

  2. UNIX网络编程——select函数的并发限制和 poll 函数应用举例

    一.用select实现的并发服务器,能达到的并发数,受两方面限制 1.一个进程能打开的最大文件描述符限制.这可以通过调整内核参数.可以通过ulimit -n来调整或者使用setrlimit函数设置,  ...

  3. 【unix网络编程第三版】阅读笔记(五):I/O复用:select和poll函数

    本博文主要针对UNP一书中的第六章内容来聊聊I/O复用技术以及其在网络编程中的实现 1. I/O复用技术 I/O多路复用是指内核一旦发现进程指定的一个或者多个I/O条件准备就绪,它就通知该进程.I/O ...

  4. UNIX网络编程——I/O复用:select和poll函数

    我们看到TCP客户同时处理两个输入:标准输入和TCP套接字.我们遇到的问题是就在客户阻塞于(标准输入上)fgets调用,服务器进程会被杀死.服务器TCP虽然正确的给客户TCP发送了一个FIN,但是既然 ...

  5. 网络编程中select模型和poll模型学习(linux)

    一.概述 并发的网络编程中不管是阻塞式IO还是非阻塞式IO,都不能很好的解决同时处理多个socket的问题.操作系统提供了复用IO模型:select和poll,帮助我们解决了这个问题.这两个函数都能够 ...

  6. UNIX网络编程 第6章 I/O复用:select和poll函数

    UNIX网络编程 第6章 I/O复用:select和poll函数

  7. Linux网络编程——tcp并发服务器(poll实现)

    想详细彻底地了解poll或看懂下面的代码请参考<Linux网络编程——I/O复用之poll函数> 代码: #include <string.h> #include <st ...

  8. Linux 网络编程的5种IO模型:多路复用(select/poll/epoll)

    Linux 网络编程的5种IO模型:多路复用(select/poll/epoll) 背景 我们在上一讲 Linux 网络编程的5种IO模型:阻塞IO与非阻塞IO中,对于其中的 阻塞/非阻塞IO 进行了 ...

  9. python select网络编程详细介绍

    刚看了反应堆模式的原理,特意复习了socket编程,本文主要介绍python的基本socket使用和select使用,主要用于了解socket通信过程 一.socket模块 socket - Low- ...

  10. Linux网络编程-IO复用技术

    IO复用是Linux中的IO模型之一,IO复用就是进程预先告诉内核需要监视的IO条件,使得内核一旦发现进程指定的一个或多个IO条件就绪,就通过进程进程处理,从而不会在单个IO上阻塞了.Linux中,提 ...

随机推荐

  1. MySQL索引最左原则:从原理到实战的深度解析

    MySQL索引最左原则:从原理到实战的深度解析 一.什么是索引最左原则? 索引最左原则是MySQL复合索引使用的核心规则,简单来说: "当使用复合索引(多列索引)时,查询条件必须从索引的最左 ...

  2. 关于我这周学习SQL注入的一些笔记:

    sql注入的原理: 通过恶意的SQL语句插入到应用的输入参数中,再在后台数据库服务器上解析执行的攻击.   Web程序的三层结构: 界面层( User Interface layer ) 业务逻辑层( ...

  3. Cannot find config.m4. phpize 安装扩展出错

    原因 编译扩展包下面的名字可能不是 config.m4,也有可能有类似 config0.m4 的文件:因此名字不一样也是找不到的,我们需要用 mv config0.m4 config.m4 :修改文件 ...

  4. Flask快速入门1

    因为新换了一个工作,项目使用了Flask框架技术,需要快速学习下,学过Django这个重量级的框架基础后,再去学习Flask框架相对还是容易的. 当然入门基础容易,要深入理解还是要慢慢花时间深耕练习使 ...

  5. python web服务器--WSGI/ASGI协议--web框架,三者之间的关系

    在 Python Web 开发中,Web 服务器.WSGI/ASGI 协议 和 Web 框架 是三个核心组成部分,它们共同协作以实现完整的 Web 应用程序.以下是三者之间的关系和作用的详细讲解: 1 ...

  6. Docker镜像(image)详解

    如果曾经做过 VM 管理员,则可以把 Docker 镜像理解为 VM 模板,VM 模板就像停止运行的 VM,而 Docker 镜像就像停止运行的容器:而作为一名研发人员,则可以将镜像理解为类(Clas ...

  7. win10/11 禁用移动热点,无法启用

    将网络重制即可

  8. 掌握 K8s Pod 基础应用 (二)

    Pod生命周期 我们一般将pod对象从创建至终的这段时间范围称为pod的生命周期,它主要包含下面的过程: pod创建过程 运行初始化容器(init container)过程 运行主容器(main co ...

  9. JDK8-时间格式化类-时区类-工具类--java进阶day07

    1.时间格式化类:DateTimeFormatter 1.创建方式 使用DateTimeFormatter调用ofPattern方法即可 . 2.格式化方法 创建好DateTimeFormatter对 ...

  10. python 工具uv

    以下是 Python 环境管理工具 uv 从入门到精通的系统性指南,基于 2025 年最新版本特性整理: 一.uv 核心优势与适用场景 极速性能 • 基于 Rust 开发,依赖解析速度比传统工具快 1 ...