IO复用之——poll

一. 关于poll

    对于IO复用模型,其优点无疑是免去了对一个个IO事件就绪的等待,转而代之的是同时对多个IO数据的检测,当检测等待的事件中至少有一个就绪的时候,就会返回告诉用户进程“已经有数据准备好了,快看看是哪个赶紧处理”,而对于IO复用的实现,除了可以用select函数,另外一个函数仍然支持这种复用IO模型,就是poll函数;



二. poll函数的用法

    虽然同样是对多个IO事件进行检测等待,但poll和select多少还是有些不同的:

wKiom1dHB9mAvCb_AAAZZrdUWaM197.png

函数参数中,

先来说nfds,这个是指当前需要关心的文件描述符的个数;

timeout同样是设置超时时间,只是和select的timeout是一个结构体不一样,这里只是一个整型类型,且含义是毫秒

fds是一个结构体指针,如下:

wKioL1dHCM_xen90AAAdbbZXhd4736.png

结构体中,

fd表示所要关心的文件描述符;

events表示该文件描述符所关心的事件,这是一个输入型参数,要告诉操作系统这个文件描述符对应的事件所关心的操作事件是什么,比如读或写;

revents是一个输出型参数,表示当poll返回时告诉用户什么操作事件是就绪的,比如如果POLLIN是就绪的,那么返回时revent的值就是POLLIN,告诉用户fd事件的POLLIN是就绪的;

events和revents的值可以为如下:

wKioL1dHDDKzCxZ_AAAQQ7FNxqY876.png这里要说明选项其实不止这三个,只是这里的讨论中这三个选项是最常用的;

events设置为POLLIN表示fd所需要读取数据,而revents若返回POLLIN则表示data已经ready可以读取了;

同样,events设置为POLLOUT表示fd所关心数据的写入,而revents返回POLLOUT则表示写事件就绪可以进行数据的写入;

至于POLLPRI,后面的解释是作为紧急选项来设置的,在TCP协议报文中有个URG的紧急指针是表示先从紧急数据的地方开始读取,这里也是这个意思;



三. 栗子时间

    同样的,这里可以用poll来编写基于TCP协议的server端和client端的数据通信,和select一样,避免使用多进程和多线程的方式转而使用对多个IO接口的等待:


server服务器端:

#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <poll.h>#include <unistd.h>#include <string.h>#define _BACKLOG_ 5//网络中连接请求队列中的最大等待数目#define _NUM_ 10//IO事件结构体数组大小void usage(const char *argv)//命令行参数的差错判断{    printf("%s   [ip]   [port]\n", argv);    exit(0);}static int CreateListenSocket(int ip, int port)//创建监听套接字{    int sock = socket(AF_INET, SOCK_STREAM, 0);     if(sock < 0)    {           perror("socket");        exit(1);    }       struct sockaddr_in server;//设置本地网络地址信息    server.sin_family = AF_INET;    server.sin_port = htons(port);    server.sin_addr.s_addr = ip;     if(bind(sock, (struct sockaddr*)&server, sizeof(server)) < 0)//绑定    {           perror("bind");        exit(2);    }        if(listen(sock, _BACKLOG_) < 0)//监听    {        perror("listen");        exit(3);    }    return sock;}int main(int argc, char *argv[]){    if(argc != 3)        usage(argv[0]);    int port = atoi(argv[2]);    int ip = inet_addr(argv[1]);    int listen_sock = CreateListenSocket(ip, port);//获取监听socket    struct sockaddr_in client;//用于存放client端的网络地址信息    socklen_t client_len = sizeof(client);    struct pollfd fds[_NUM_];//用一个结构体数组来存放各种IO事件    fds[0].fd = listen_sock;//将监听套接字放入数组中    fds[0].events = POLLIN;//将监听事件需要的event设置为读取数据POLLIN    fds[0].revents = 0;    size_t i = 1;    for(; i < _NUM_; ++i)//初始化结构体数组中成员    {        fds[i].fd = -1;        fds[i].events = 0;        fds[i].revents = 0;    }    int max_fd = 1;//设置最大的文件描述符个数    int timeout = 5000;//设置超时时间        while(1)    {        switch(poll(fds, max_fd, timeout))        {            case -1://出错                perror("poll");                break;            case 0://超时                printf("timeout...\n");                break;            default://至少有一个事件已经就绪,可以进行IO数据的操作                {                    size_t i = 0;                    for(; i < _NUM_; ++i)                    {                            //判断是否为监听事件就绪,如果是代表有连接请求需要处理                        if((fds[i].fd == listen_sock) && (fds[i].revents == POLLIN))                        {                                //处理连接                            int accept_sock = accept(listen_sock, (struct sockaddr*)&client, &client_len);                            if(accept_sock < 0)                            {                                perror("accept");                                continue;                            }                            printf("connect with a client... [ip]:%s  [port]:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));                            size_t i = 0;                            //将创建出新的数据传输socket文件描述符设置进poll结构体数组                            for(; i < _NUM_; ++i)                            {                                if(fds[i].fd == -1)                                {                                    fds[i].fd = accept_sock;                                    fds[i].events = POLLIN;//将事件设置为需要读取数据                                    max_fd++;//更新最大文件描述符个数                                    break;                                }                                                            }                            if(i == _NUM_)                                close(accept_sock);//若超出数组个数则表明当前无法处理,关闭掉                        }//表示为其他除了listen socket之外的要进行数据传输的socket                        else if((fds[i].fd > 0) && (fds[i].revents == POLLIN))                        {                            char buf[1024];                            //进行数据的读取                            ssize_t size = read(fds[i].fd, buf, sizeof(buf)-1);                            if(size < 0)//读取出错                                perror("read");                            else if(size == 0)//客户端关闭,将结构体数组中相应位置清空                            {                                 printf("client closed...\n");                                 //将要关闭的文件描述符和结构体数组中最后一个有效的文件描述符进行交换                                 //确保当前有效的文件描述符在数组内都是连续的                                                                 struct pollfd tmp = fds[i];                                                                 fds[i] = fds[max_fd-1];                                                                 fds[max_fd-1] = tmp;                                                                                                                                  //交换后要删除的就在数组的最后,关闭且将数组内相应位置为无效值                                                                 close(fds[max_fd-1].fd);                                                                 fds[max_fd-1].fd = -1;                                                                  fds[max_fd-1].events = 0;                                                                 fds[max_fd-1].revents = 0;                                                                 --max_fd;//更新当前有效文件描述符的个数                            }                            else//读取成功,输出数据                            {                                buf[size]  ='\0';                                printf("client# %s\n", buf);                            }                        }                        else                        {}                    }                }                break;        }    }    return 0;}

client客户端这里就不写了,和基于TCP协议的socket编程中的client是差不多的;


运行程序:

wKiom1dINs6TtO_ZAAAQyAzXPhg489.png


这里需要说明的是:

  1. 和select不同,select是用一个fd_set数据类型来存放各个需要操作的IO文件描述符,这里的poll是用一个结构体来存放文件描述符和锁关心的事件类型,因此,poll并没有处理文件描述符的上限,但相同的是,每一次poll返回都仍然需要遍历来获取事件就绪的位置以此来进行相应的处理,还是一样有复制和系统遍历带来的额外开销,当处理事件比较多的时候仍然是低效的;

  2. 在每一次循环到select之前都需要将对应事件的fd_set的集合调用FD_ZERO函数来进行重新的初始化清零,因为不是会有文件描述符的新建和关闭,需要进行初始化然后再将事件重新一一设置进相应的fd_set里面;而poll并不需要,因为使用一个结构体数组来管理相当于结合了select中的设置数组存放文件描述符和添加设置这两步,每一次进行poll的时候都会将发生就绪的事件对应的revents置位,当处理完毕就会被系统自动归为0,并不需要进行手动初始化清零;

  3. 在结构体数组还未使用之前,和被刚定义的变量一样,结构体成员fd、events和revents都是随机值,虽然后来使用的时候都会被赋对应的有效值,但为了避免判断时的二义性问题最好还是在循环使用前都将其初始化为统一的可识别的无效值;



《完》

本文出自 “敲完代码好睡觉zzz” 博客,请务必保留此出处http://2627lounuo.blog.51cto.com/10696599/1783922

### 多路IO复用的技术原理 多路IO复用的核心在于允许单个线程同时监控多个文件描述符的状态变化,从而提高资源利用率并减少上下文切换带来的性能损耗。其基本工作流程是通过调用特定的操作系统API来注册感兴趣的事件集合(如可读、可写或异常条件),当任何一个文件描述符上的状态发生变化时,操作系统会通知应用程序[^1]。 具体来说,在多路IO复用中,程序向操作系统提交一组文件描述符列表,并指定希望监听的事件类型。随后,程序进入等待状态直到至少有一个文件描述符准备好执行相应操作为止。这种机制避免了传统阻塞式I/O模型中的长时间挂起问题,同时也克服了信号驱动I/O可能引发的频繁中断开销较大的缺点[^3]。 对于Linux平台而言,epoll作为新一代高效边缘触发模式下的接口提供了更优解法相比早期版本select/poll函数有更好的扩展性更低延迟表现形式因为采用了红黑树结构存储关注对象以及基于事件回调机制减少了不必要的扫描次数提高了整体效率水平[未标注新引用]。 ### 实现方式 #### 使用 `select` 函数 ```c fd_set readfds; FD_ZERO(&readfds); FD_SET(socket_fd, &readfds); struct timeval timeout; timeout.tv_sec = 5; // 设置超时时间为5秒 timeout.tv_usec = 0; int activity = select(max_socket_fd + 1, &readfds, NULL, NULL, &timeout); if (activity < 0) { perror("select error"); } else if (activity == 0) { printf("Timeout occurred! No data after 5 seconds.\n"); } else { if(FD_ISSET(socket_fd,&readfds)) { recv_data(); } } ``` 此代码片段展示了如何利用 `select` 来监视单一 socket 的可读性情况。然而由于每次都需要重新构建 fd 集合并且遍历整个数组查找已准备好的连接所以不适合大规模并发场景下应用。 #### 使用 `poll` 函数 ```c struct pollfd fds[NUM_FDS]; for(int i=0;i<NUM_FDS;i++) { fds[i].fd = sockets[i]; fds[i].events = POLLIN | POLLPRI ; } int ret = poll(fds , NUM_FDS , TIMEOUT_MS ); switch(ret){ case -1 : /* handle error */ break ; default : // process ready descriptors here... } ``` 这里给出了另一个例子说明怎样借助于 `poll` API 完成相似功能但支持更多数量级的句柄数目而且不必像前者那样受限最大值参数影响不过仍然保持O(n)复杂度因此也不够理想针对极高负载环境需求[^未定义]. #### 利用 `epoll` 提升效能 ```c #include <sys/epoll.h> const int MAX_EVENTS = 10; int epoll_fd = epoll_create1(0); struct epoll_event event; event.data.fd = listen_sock; event.events = EPOLLIN | EPOLLET; // 边缘触发型 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_sock, &event); while(true){ struct epoll_event events[MAX_EVENTS]; int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); for(int n=0;n<nfds;++n){ if(events[n].data.fd==listen_sock){ accept_new_connection(); }else{ handle_client_request(events[n].data.fd); } } } ``` 上述实例体现了采用 Linux 特有的高性能解决方案——即 epoll ——来进行 I/O 路由管理的方法论优势明显体现在三个方面分别是更高的吞吐量更快响应速度还有更好的伸缩能力特别适合处理海量短生命周期请求场合之中[^自增序号]. ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值