I/O多路复用的实现机制 - poll 用法总结

一、基本知识

poll的多路复用机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询(polling),根据描述符的状态进行处理,但是poll没有最大文件描述符数量上的限制。

二、poll函数

poll函数的原型声明:

//使用:man 2 poll,查看poll函数的使用帮助信息(CentOS-7.6)
#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

#define _GNU_SOURCE         /* See feature_test_macros(7) */
#include <poll.h>

int ppoll(
struct pollfd *fds,
nfds_t nfds,
const struct timespec *timeout_ts,
const sigset_t *sigmask);

【参数说明】

(1)第1个参数fds:是一个struct pollfd结构体类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。pollfd结构体的定义如下:

struct pollfd
{
	int fd;  	//文件描述符
	short events;   //等待的事件
	short revents;  //实际发生的事件,由内核填充
};

每一个struct pollfd结构体指定了一个被监视的描述符,可以传递多个结构体,指示poll监视多个文件描述符,没有数量限制,由参数fds指针指向一个struct pollfd结构体数组来实现。要测试的I/O事件由events成员指定,poll函数在相应的revents成员中返回该描述符的状态。(每个描述符都有两个变量,一个为调用值,另一个为返回结果值,从而避免了select中使用值-结果参数,select函数的中间3个参数都是值-结果参数)。其中,events成员是监视该描述符的事件掩码,由用户自己来设置该值;revents成员是描述符的操作结果事件掩码,内核在调用返回时设置这个成员的值。

events成员中请求的任何事件都可能在revents成员中返回。下图中列出了用于指定events标志以及测试revnets标志的一些常值:

上图中分为了3个部分:第1部分是处理输入的4个常值,第二部分是处理输出的3个常值,第3部分是处理错误的3个常值。其中第3部分的3个常值不能在events中设置,但是当相应条件存在时就会在revents中返回。

<说明> 上表中列举的符号常量定义在/usr/include/bits/poll.h文件中,参考的是CentOS-7.6系统。

/* Event types that can be polled for.  These bits may be set in `events'
   to indicate the interesting event types; they will appear in `revents'
   to indicate the status of the file descriptor.  */
#define POLLIN      0x001       /* There is data to read.  */
#define POLLPRI     0x002       /* There is urgent data to read.  */
#define POLLOUT     0x004       /* Writing now will not block.  */

#if defined __USE_XOPEN || defined __USE_XOPEN2K8
/* These values are defined in XPG4.2.  */
# define POLLRDNORM 0x040       /* Normal data may be read.  */
# define POLLRDBAND 0x080       /* Priority data may be read.  */
# define POLLWRNORM 0x100       /* Writing now will not block.  */
# define POLLWRBAND 0x200       /* Priority data may be written.  */
#endif

#ifdef __USE_GNU
/* These are extensions for Linux.  */
# define POLLMSG    0x400
# define POLLREMOVE 0x1000
# define POLLRDHUP  0x2000  //(since Linux 2.6.17)
#endif

/* Event types always implicitly polled for.  These bits need not be set in
   `events', but they will appear in `revents' to indicate the status of
   the file descriptor.  */
#define POLLERR     0x008       /* Error condition.  */
#define POLLHUP     0x010       /* Hung up.  */
#define POLLNVAL    0x020       /* Invalid polling request.  */

poll识别3类数据:普通(Normal)、优先级带(Priority Band)和高优先级(High Priority)。例如,我们要同时监视一个文件描述符的可读和可写事件,可以将events设置为POLLIN | POLLOUT。当poll函数返回时,我们可以检查revents中的标志:

可读:items[i].revents & POLLIN

可写:items[i].revents & POLLOUT

如果POLLIN事件被设置,则文件描述符可以读取而不导致阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。

(2)第2个参数:nfds,表示的是被监控的描述符的个数,亦即fds指针指向的struct pollfd结构体数组的元素个数。

<说明> 历史上这个参数曾被定义成功无符号整型(unsigned long),似乎过分大了,定义为无符号整型(unsigned int)可能就足够了。Unix98为该参数定义了名为nfds_t的新数据类型。在/usr/include/sys/poll.h文件中该数据类型的定义如下:

typedef unsigned long int nfds_t;  //CentOS7.6中,该数据类型是被定义为无符号长整型的

(3)第3个参数:timeout,指定poll函数返回前等待超时的时间,单位是毫秒数。下表给出了它的可能取值:

<说明>

(1)如果timeout > 0 或者 为负值(一般设置为-1)时,poll函数将会被阻塞,直到被监控的描述符指定的I/O事件准备就绪或者发生错误时,poll才会返回;或者定时器到时也会返回(在timeout>0的情况下)。

(2)timeout=0时,poll函数立刻返回,不阻塞进程,无论是否有描述符准备就绪。

【返回值】

1、成功,返回已就绪的描述符个数,即返回struct pollfd结构体中revents成员值非0的描述符个数;

2、若定时器到时之前没有任何描述符就绪,则返回0。

3、当发生错误时,返回值为-1,并设置相应的错误码给errno全局变量。错误码的可能取值如下:

  • EFAULT:fds指针指向的结构体数组的地址超出进程的地址空间。
  • EINTR:在请求事件发生前产生了一个信号事件。
  • EINVAL:nfds的值超出了RLIMIT_NOFILE 的值。
  • ENOMEM:没有多余的内存空间分配描述符表。

<说明> 如果我们不再关心某个特定描述符,那么可以把与它对应的struct pollfd结构体的fd成员设置为一个负值(一般而言设置为-1)。poll函数将忽略这样的pollfd结构的events成员,同时返回时将它的revents成员的值置为0。相比于select函数,poll函数不再有FD_SETSIZE最大描述符数目的设定,因为分配一个pollfd结构体数组并把该数组中元素个数通知内核就行了,内核不再需要知道类似fd_set的固定大小的数据类型。

事实上,传递给select函数的fd_set结构体类型变量的成员是一个整型数组,不过它的数组长度是个固定值,是由操作系统内部定义的FD_SETSIZE NFDBITS 这两个符号常量决定的,无法人为修改;而传递给poll函数的pollfd结构体数组,其结构体数组的长度是可以人为设定的。

四、poll 与 select 的对比及其缺陷

poll 与 select最大的区别就是poll没有最大描述符数量的限制,因此它仍然存在和select同样的缺陷。

(1)和select函数一样,poll同样需要维护一个用来存放描述符的数据结构,当描述符的数量比较大时,会使得用户空间和内核空间在传递该数据结构时复制开销大。

(2)poll 和 select一样,对描述符进行扫描的方式也是线性扫描,每次调用poll都需要遍历整个描述符集,不管那个描述符是不是活跃的,都需要遍历一遍。当描述符数量较多时,会占用大量CPU资源。

 (3)poll 和 select一样,不是线程安全的函数。

五、示例程序

程序描述:编写一个echo server程序,功能是客户端向服务器发送信息,服务器端接收数据后输出并原样返回给客户端,客户端接收到消息并输出到终端。代码如下:

  • 公共头文件:socket_common.h
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

#include <netinet/in.h>
#include <sys/socket.h>
#include <poll.h>
#include <unistd.h>
#include <sys/types.h>
#include <arpa/inet.h>

//#define IPADDRESS   "127.0.0.1"
//#define PORT        8787
#define MAXLEN      1024
#define LISTENQ     5
#define OPEN_MAX    1000
#define INFTIM      -1
  • 服务端程序:poll_server.c
/**
服务器端程序
*/
#include "socket_common.h"
//函数声明
static int prepare_tcp_listen(const char *ip,int port);
static void do_poll(int listenfd);
static void handle_client(struct pollfd *connfds,int count);

int main(int argc,char *argv[])
{
    int sfd;
    if(argc < 2)
    {
        printf("usage: ./poll_server port\n");
        exit(-1);
    }
    sfd=prepare_tcp_listen(NULL,atoi(argv[1]));
    do_poll(sfd);
    return 0;
}

static int prepare_tcp_listen(const char *ip,int port)
{
    //创建socket套接字
    int sfd=socket(AF_INET,SOCK_STREAM,0);
    if(sfd == -1)
    {
        perror("socket");
        exit(-1);
    }
    struct sockaddr_in server_addr;
    bzero(&server_addr,sizeof(struct sockaddr_in));
    //填充sockaddr_in结构体内容
    server_addr.sin_family=AF_INET;
    server_addr.sin_port=htons(port);
    //server_addr.sin_addr.s_addr=inet_addr(ip);
    server_addr.sin_addr.s_addr=INADDR_ANY;
    //绑定IP地址和端口号
    if(bind(sfd,(struct sockaddr*)&server_addr,sizeof(struct sockaddr)) == -1)
    {
        perror("bind");
        close(sfd);
        exit(-1);
    }
    //监听客户机的连接请求
    if(listen(sfd,LISTENQ) == -1)
    {
        perror("listen");
        close(sfd);
        exit(-1);
    }
    return sfd;
}

static void do_poll(int listenfd)
{
    int new_fd;
    struct pollfd clitfds[OPEN_MAX];
    struct sockaddr_in client_addr;
    socklen_t clitaddrlen=sizeof(client_addr);
    int imax,i,nready;
    //初始化客户端连接描述符
    for(i=0;i<OPEN_MAX;i++)
        clitfds[i].fd=-1;
    //初始添加第一个监听文件描述符
    clitfds[0].fd=listenfd;
    clitfds[0].events=POLLIN;
    imax=0;
    //循环处理
    for(;;)
    {
        //获取可用文件描述符的个数
        nready=poll(clitfds,imax+1,INFTIM); //INFTIM=-1,表示永远等待
        if(nready == -1)
        {
            perror("poll");
            exit(-1);
        }
        //监测监听文件描述符是否准备好
        if(clitfds[0].revents & POLLIN) //监听事件
        {
            //接受客户端连接请求事件
            if((new_fd=accept(listenfd,(struct sockaddr*)&client_addr,&clitaddrlen)) == -1)
            {
                if(errno == EINTR)  //EINTR:系统调用被信号中断。
                    continue;
                else
                {
                    perror("accept");
                    exit(-1);
                }
            }
            fprintf(stdout,"accept a new client: %s:%d\n",
                inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
            //将新的连接文件描述符添加到clitfds结构体数组中
            for(i=1;i<OPEN_MAX;i++)
            {
                if(clitfds[i].fd<0)
                {
                    //添加连接文件描述符到读描述符集合中
                    clitfds[i].fd=new_fd;
                    //将新的连接文件描述符添加到读描述符集合中
                    clitfds[i].events=POLLIN;
                    break;
                }
            }
            if(i == OPEN_MAX)
            {
                fprintf(stderr,"too many clients!\n");
                exit(-1);
            }
            //记录客户连接套接字的个数
            imax=(i>imax)?i:imax;
            if(--nready <= 0)
                continue;
        }
        printf("connect success client num=%d\n",imax);
        //处理与客户端的通信过程
        handle_client(clitfds,imax);
    }
}

static void handle_client(struct pollfd *connfds,int count)
{
    int i,len;
    char buf[MAXLEN];
    bzero(buf,sizeof(buf));
    //扫描整个文件描述符的集合状态,检测有无就绪的文件描述符
    for(i=1;i<=count;i++)
    {
        if(connfds[i].fd<0)
            continue;
        //检测客户端文件描述符是否准备好
        if(connfds[i].revents & POLLIN)
        {
            //接收客户端发送过来的消息
            if((len=read(connfds[i].fd,buf,MAXLEN)) == 0)
            {
                close(connfds[i].fd);
                connfds[i].fd=-1;
                continue;
            }
            write(STDOUT_FILENO,buf,len); //输出到终端屏幕
            //向客户端发送buf内容
            write(connfds[i].fd,buf,len);
        }
    }
}

服务端程序说明:服务端有两个文件描述符,一个是监听客户端连接请求的文件描述符listen_fd,另一个是处理客户端读写操作的文件描述符new_fd,每当有新的客户端连接上来的时候,就将新的new_fd添加到pollfd结构体数组clientfds当中,同时受监控的文件描述符数目加1。

  • 客户端程序:poll_client.c
/**
客户端程序
*/
#include "socket_common.h"
//函数声明
int tcp_connect(const char *ip,int port);
static void handle_connection(int sockfd);

int main(int argc,char *argv[])
{
    if(argc < 3)
    {
        printf("usage: ./poll_client ip port\n");
        exit(-1);
    }
    int cfd=tcp_connect(argv[1],atoi(argv[2]));
    //处理连接描述符
    handle_connection(cfd);
    return 0;
}

//用于客户端向服务器端发起连接
int tcp_connect(const char *ip,int port)
{
    int cfd=socket(AF_INET,SOCK_STREAM,0);
    if(cfd == -1)
    {
        perror("socket");
        exit(-1);
    }
    struct sockaddr_in server_addr;
    memset(&server_addr,0,sizeof(struct sockaddr_in));
    server_addr.sin_family=AF_INET;
    server_addr.sin_port=htons(port);
    server_addr.sin_addr.s_addr=inet_addr(ip);
    //将cfd连接到制定的服务器网络地址server_addr
    if(connect(cfd,(struct sockaddr*)&server_addr,sizeof(struct sockaddr)) == -1)
    {
        perror("connect");
        close(cfd);
        exit(-1);
    }
    return cfd;
}

static void handle_connection(int sockfd)
{
    char sendbuf[MAXLEN],recvbuf[MAXLEN];
    struct pollfd pfds[2];
    int len;
    //添加连接描述符
    pfds[0].fd=sockfd;
    pfds[0].events=POLLIN;
    //添加标准输入描述符
    pfds[1].fd=STDIN_FILENO;
    pfds[1].events=POLLIN;
    for(;;) //循环处理
    {
        if(poll(pfds,2,-1) < 0)
        {
            perror("poll");
            exit(-1);
        }
        //接收从服务器端发送过来的消息
        if(pfds[0].revents & POLLIN)
        {
            if((len=read(sockfd,recvbuf,MAXLEN)) == 0)
            {
                fprintf(stderr,"client:server has closed!\n");
                close(sockfd);
                exit(-1);
            }
            write(STDOUT_FILENO,recvbuf,len); //标准输出
        }
        //测试标准输入是否准备好
        if(pfds[1].revents & POLLIN)
        {
            if((len=read(STDIN_FILENO,sendbuf,MAXLEN)) == 0) //标准输入
            {
                shutdown(sockfd,SHUT_WR); //终止socket通信,关闭连接的写这一半
                continue;
            }
            write(sockfd,sendbuf,len); //发送消息给服务器端
        }
    }
}

 客户端程序说明:客户端程序设置了两个文件描述符,一个是用于监控来自服务端的可读数据;另一个是监控标准输入端的可读数据。poll函数监控这两个描述符的可读事件,可以看到,我们设置的超时条件是永久等待,在这两个描述符的可读I/O事件未就绪时,客户端进程将一直处于阻塞状态。

  • Makefile
#第1种方式
all:poll_server poll_client

poll_server:poll_server.o
	gcc poll_server.o -o poll_server
poll_client:poll_client.o
	gcc poll_client.o -o poll_client

poll_server.o:poll_server.c
	gcc -c poll_server.c -o poll_server.o
poll_client.o:poll_server.c
	gcc -c poll_client.c -o poll_client.o

clean:
	rm -rf ./*.o ./poll_server ./poll_client

 该示例程序本人已经测试通过了的。

题外话

由于poll的多路复用机制仍然存在诸多问题,于是5年以后, 在2002, 大神 Davide Libenzi 实现了epoll。epoll 可以说是I/O多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题,比如:epoll 现在是线程安全的,epoll不仅会告诉描述符集中是否有描述符准备就绪,还会告诉你是哪个描述符准备就绪了,不用自己去找了。在下一篇博文中,会详细介绍epoll的用法。

参考

IO多路复用之poll总结

《UNIX网络编程卷1:套接字联网API(第3版)》第6.10章节

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值