Linux网络编程 - 另一种I/O多路复用:poll

Source

上一讲我们讲到了 I/O 多路复用技术,并以 select 为核心,展示了 I/O 多路复用技术的能力。select 方法是多个 UNIX 平台支持的非常常见的 I/O 多路复用技术,但是它有一个缺点,那就是所支持的文件描述符的个数是有限的。在 Linux 系统中,select 的默认最大值为 1024。

poll 函数

和 select 相比,poll 和内核交互的数据结构有所变化,另外,也突破了文件描述符的个数限制。

int poll(struct pollfd *fds, unsigned long nfds, int timeout);   
返回值:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
struct pollfd {
    int    fd;       /* file descriptor */
    short  events;   /* events to look for */
    short  revents;  /* events returned */
 };

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

#define    POLLIN    0x0001    /* any readable data available */
#define    POLLPRI   0x0002    /* OOB/Urgent readable data */
#define    POLLOUT   0x0004    /* file descriptor is writeable */

和 select 不同的地方在于,poll 每次检测之后的结果不会修改原来的传入值,而是将结果保留在 revents 字段中,这样就不需要每次检测完都得重置待检测的描述字和感兴趣的事件。我们可以把 revents 理解成“returned events”。

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 */

一般我们在程序里面有 POLLIN 即可。套接字可读事件和 select 的 readset 基本一致,是系统内核通知应用程序有数据可以读,通过 read 函数执行操作不会被阻塞。

第二类是可写事件,有以下几种:

#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 函数执行写操作不会被阻塞。

以上两大类的事件都可以在“returned events”得到复用。还有另一大类事件,没有办法通过 poll 向系统内核递交检测请求,只能通过“returned events”来加以检测,这类事件是各种错误事件。

#define POLLERR    0x0008    /* 一些错误发送 */
#define POLLHUP    0x0010    /* 描述符挂起*/
#define POLLNVAL   0x0020    /* 请求的事件无效*/

我们再回过头看一下 poll 函数的原型。参数 nfds 描述的是数组 fds 的大小,简单说,就是向 poll 申请的事件检测的个数。

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

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

poll 函数有一点非常好,如果我们不想对某个 pollfd 结构进行事件检测可以把它对应的 pollfd 结构的 fd 成员设置成一个负值。这样,poll 函数将忽略这样的 events 事件,检测完成以后,所对应的“returned events”的成员值也将设置为 0。

我们发现 poll 函数和 select 不一样的地方就是,在 select 里面,文件描述符的个数已经随着 fd_set 的实现而固定,没有办法对此进行配置;而在 poll 函数里,我们可以控制 pollfd 结构的数组大小,这意味着我们可以突破原来 select 函数最大描述符的限制,在这种情况下,应用程序调用者需要分配 pollfd 数组并通知 poll 函数该数组的大小。

基于 poll 的服务器程序

#define INIT_SIZE 128

int main() 
{
    int listenfd, connfd;
    int ready_number;
    ssize_t n;
    char buf[MAXLINE];
    struct servaddr_in cli_addr;

    listenfd = socket(PF_INET, SOCK_STREAM, 0);

    bzero(&serv_addr, sizeof(serv_addr));
    bzero(&cli_addr, sizeof(cli_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(7878);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
    bind(listenfd, (struct sockaddr*) &serv_addr, sizeof(serv_addr));
    listen(listenfd, SOMAXCONN);

    /*初始化pollfd数组,这个数组的第一个元素是listenfd,其余的用来记录将要连接的connfd
     数组的大小固定为 INIT_SIZE,这在实际的生产环境肯定是需要改进的。*/
    struct pollfd event_set[INIT_SIZE];
    /*将监听套接字 listen_fd 和对应的 POLLRDNORM 事件加入到 event_set 里,表示我们期望系统内核    
      检测监听套接字上的连接建立完成事件*/
    event_set[0].fd = listenfd;
    event_set[0].events = POLLRDNORM;

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

    for (;;) {
        //timeout 设置为 -1,表示在 I/O 事件发生之前 poll 调用一直阻塞。
        if ((ready_number = poll(event_set, INIT_SIZE, -1)) < 0) {
            printf("poll failed \n");
            continue;
        }

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

            //找到一个可以记录该连接套接字的位置
            for (i = 1; i < INIT_SIZE; i++) {
                if (event_set[i].fd < 0) {
                    /*把连接描述字 connect_fd 也加入到 event_set 里,而且说明了我们感兴趣的事件类型为 POLLRDNORM,也就是套集字上有数据可以读*/
                    event_set[i].fd = connfd;
                    event_set[i].events = POLLRDNORM; //POLLIN包括了OOB等带外数据的检测,POLLRDNORM则不包括这部分。
                    break;
                }
            }

            if (i == INIT_SIZE) {
                printf("can not hold so many clients\n");
            }
            /*如果处理完监听套接字之后,就已经完成了这次 I/O 复用所要处理的事情,那么我们就可以跳过后面的处理,再次进入 poll 调用。*/
            if (--ready_number <= 0)
                continue;
        }

        /*查看 event_set 里面其他的事件,也就是已连接套接字的可读事件。这是通过遍历 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)) {
                if ((n = read(socket_fd, buf, MAXLINE)) > 0) {
                    if (write(socket_fd, buf, n) < 0) {
                        printf("write error\n");
                    }
                } else if (n == 0 || errno == ECONNRESET) {
                    //如果读到 EOF 或者是连接重置,则关闭这个连接,并且把 event_set 对应的 pollfd 重置
                    close(socket_fd);
                    event_set[i].fd = -1;
                } else {
                    printf("read error\n");
                }

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

总之,poll 是另一种在各种 UNIX 系统上被广泛支持的 I/O 多路复用技术,虽然名声没有 select 那么响,能力一点不比 select 差,而且因为可以突破 select 文件描述符的个数限制,在高并发的场景下尤其占优势。

 

温故而知新 !

发布了50 篇原创文章 · 获赞 33 · 访问量 1万+