I/O多路复用——poll

上一篇我们说了关于select的相关信息,我们可以看到select是有弊端的,所以为了解决select的弊端,UNIX又在后期提出了poll。

select的弊端这里就不多说了,上一篇博客有提及。

poll


poll和select类似,不过在一些方面改善了select的弊端。它也是在指定的时间进行轮询文件描述符,查看是否有就绪时间发生。

和上次一样,我们先来看一下poll系统调用。

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

fds是一个pollfd的结构体数组。

struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };

这就是这个结构体数组每个元素。fd用来记录对应的文件描述符,events用来表示poll所监听的事件,这个由用户来设置。revents用来表示返回的事件。revents是通过内核来进行操作修改。

这里提供了一些合法事件。

事件 说明
POLLIN 普通或优先级带数据可读
POLLRDNORM 普通数据可读
POLLRDBAND 优先级带数据可读
POLLPRI 高优先级数据可读
POLLOUT 普通数据可写
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述字不是一个打开的文件

后面的三个参数在events无意义,只能作为返回结果存储在revents。

另外,这里需要说的,这些参数如何设置给events,这些宏相当于每一个占用一个比特位,我们可以去想一下位图,所以,如果我们要进行设置两个事件,就使用|操作,另外,当我们去查看事件是否发生的时候,这个时候我们可以使用revents&事件,如果事件发生了,那么结果大于1。这就是一个简单的位运算的,相信你仔细想想就能够理解。

第二个参数nfds,用来监视的文件描述符的数目。

第三个参数是timeout,用来设置超时时间。

参数 说明
-1 poll将永远阻塞,等待知道某个时间发生
0 立即返回
大于0的值 设置正常时间值

返回值
poll返回revents不为0的文件描述符的个数。
失败返回-1

总结


poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

poll使用了events和revents分流的特点,这样可以使得对关心事件只进行注册一次。

poll基于链表进行存储,没有最大连接数的限制,只取决于内存大小。

poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

poll的实现机制与select类似,其对应内核中的sys_poll,只不过poll向内核传递pollfd数组,然后对pollfd中的每个描述符进行poll,相比处理fdset来说,poll效率更高。poll返回后,需要对pollfd中的每个元素检查其revents值,来得指事件是否发生。

poll的缺点


1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样是不是有意义。

2、poll依然需要进行轮询,所消耗的时间太多。

3、水平触发,效率低

示例程序


聊天室程序:

#define _GNU_SOURCE 1
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<errno.h>
#include<string.h>
#include<poll.h>

#define LIMIT_FD 65535
#define LIMIT_USER 5
#define BUF_SIZE 1024
//客户数据:包含客户的socket地址,待写到客户端的数据的位置、从客户端读入数据。

struct client_data
{
    struct sockaddr_in address;
    char* write_buf;    //写入客户端段的数据的位置
    char buf[BUF_SIZE];    //客户端读入的数据
};

int StartUp(int port,char *ip_addr)
{
    assert(ip_addr);
    int sock = socket(AF_INET, SOCK_STREAM,0);
    if(sock < 0)
    {
        perror("socket");
        exit(2);
    }

    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    local.sin_addr.s_addr = inet_addr(ip_addr);

    if(bind(sock,(struct sockaddr *)&local,sizeof(local)) < 0)
    {
        perror("bind");
        exit(3);
    }

    if(listen(sock,5) < 0)
    {
        perror("listen");
        exit(4);
    }

    return sock;
}
//设置文件描述符为非阻塞状态
int setnoblocking(int fd)
{
    int old_option = fcntl(fd, F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd,F_SETFL,new_option);
    return old_option;
}

int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        printf("Usage: %s [local_ip] [local_port]\n",argv[0]);
        return 1;
    }

    //建立监听socket 
    int listen_sock = StartUp(atoi(argv[2]),argv[1]);

    //创建users数组,分配client数据对象的文件描述符。利用它来进行索引用户数据以及发数据
    struct client_data* users = (struct client_data *)malloc(sizeof(struct client_data)*LIMIT_FD);
    //client_data users[LIMIT_FD];

    //虽然有足够多的client_data,但是依然要限制用户数量,事件最大
    struct pollfd fds[LIMIT_USER+1];

    int user_count = 0;
    int i = 0;

    for(i = 1; i <= LIMIT_USER; ++i)
    {
        fds[i].fd = -1;
        fds[i].events = 0;
    }

    fds[0].fd = listen_sock;//设置监听端口
    fds[0].events = POLLIN|POLLERR;//监听端口设置可读和错误事件
    fds[0].revents = 0;


    while(1)
    {
        //永远等待,当准备好再去提交给应用程序。
        int ret = poll(fds, user_count+1, -1);

        if(ret < 0)
        {
            printf("poll faile\n");
            break;
        }

        for(i = 0; i < user_count+1; ++i)
        {
            //此时为监听套接字,有新连接来,监听套接字接受到可读事件
            if((fds[i].fd == listen_sock) && (fds[i].revents & POLLIN))
            {
                struct sockaddr_in peer;
                socklen_t peer_len = sizeof(peer);

                int sock = accept(listen_sock,\
                                  (struct sockaddr *)&peer,&peer_len);
                if(sock < 0)
                {
                    perror("accept");
                    continue;
                }

                printf("new user :ip:%s,port:%d\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));

                // 如果请求太多,则关闭请求连接。
                if(user_count >= LIMIT_USER)
                {
                    const char *msg = "Too many users!!!\n";
                    printf("%s",msg);
                    write(sock, msg, sizeof(msg) );
                    close(sock);
                    continue;
                }
                //相对于新连接,同时去修改fds,和users数组对应连接的文件描述符sock的客户数据。
                user_count++;
                users[sock].address = peer;

                //设置非阻塞
                setnoblocking(sock);
                fds[user_count].fd = sock;
                fds[user_count].events = POLLIN | POLLERR | POLLRDHUP;
                fds[user_count].revents = 0;
                printf("come a new user, now have %d users\n",user_count);
            }
            //对于出现错误信息
            else if(fds[i].revents & POLLERR)
            {
                printf("get an error from %d\n",fds[i].fd);
                //...

                continue;
            }
            //如果客户端关闭连接。此时检测到客户端断开的请求,所以这个时候触发这个事件
            else if(fds[i].revents & POLLRDHUP)
            {
                //服务器也许要关闭连接,并且把user_count减1
                //这里的减相当于去移动了文件描述符,把最大的放到了需要减的那个了。
                //
                users[fds[i].fd] = users[fds[user_count].fd];
                close(fds[i].fd);
                fds[i] = fds[user_count];
                i--;
                user_count--;
                printf("a client left\n");
            }
//连接套接字可读
            else if(fds[i].revents & POLLIN)
            {
                int sock = fds[i].fd;
                memset(users[sock].buf, 0,sizeof(users[sock].buf));
                ret = read(sock, users[sock].buf, sizeof(users[sock].buf) - 1);

                printf("client :%s\n",users[sock].buf);
                if(ret > 0)
                {
                    //收到客户数据,此时通知其他的socket接受数据
                    users[sock].buf[ret] = 0;
                    int j = 0;
                    for(j = 1; j <= user_count; ++j)
                    {
                        if(fds[j].fd == sock)
                        {
                            continue;
                        }
                        fds[j].events |= ~POLLIN;
                        fds[j].events |= POLLOUT;

                        users[fds[j].fd].write_buf = users[sock].buf;
                    }
                }
                else if(ret < 0)
                {
                    //读取错误,关闭连接
                    if(errno != EAGAIN)
                    {
                        perror("read");
                        close(sock);
                        users[fds[i].fd] = users[fds[user_count].fd];
                        fds[i] = fds[user_count];
                        i--;
                        user_count--;
                    }
                }
            }
            else if(fds[i].revents & POLLOUT)
            {
                //连接套接字可写
                int sock =fds[i].fd;
                //判断是否可写
                if(! users[sock].write_buf)
                {
                    continue;
                }
                ret = write(sock,users[sock].write_buf,\
                            BUF_SIZE-1);
                users[sock].write_buf = NULL;
                //写完以后重新注册fds[i]的可读事件
                fds[i].events |= ~POLLOUT;
                fds[i].events |= POLLIN;
            }
            }
            }


    close(listen_sock);
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章