现在的位置: 首页 > 综合 > 正文

poll,epoll,select比较分析

2019年03月20日 ⁄ 综合 ⁄ 共 5243字 ⁄ 字号 评论关闭

非阻塞 I/O 经常使用 poll(System V)、select(BSD Unix)、 epoll(linux2.5.45开始)系统调用。


select系统调用

select()的调用形式为:
    #include <sys/select.h>
    #include <sys/time.h>
    int select(int maxfd,                   /*要被检测的比特数,待检测的最大文件描述符大1*/
                 fd_set *readfds,             /*被读监控的文件描述符集*/
                 fd_set *writefds,            /*被读监控的文件描述符集*/
                 fd_set *exceptfds,         /*被例外条件监控的文件描述符集*/
                 const struct timeval *timeout);/*定时器的作用*/

   参数timeout到了指定的时间,无论是否有设备准备好,都返回调用。timeval的结构定义如下:
    struct timeval{
        long tv_sec; //秒
        long tv_usec; //微秒
    }

    timeout取不同的值,该调用就表现不同的性质:
    timeout为0,调用立即返回
    timeout为NULL,select()调用就阻塞,直到知道有文件描述符就绪;
    timeout为正整数,就是一般的定时器

    select调用返回时,除了那些已经就绪的描述符外,select将清除readfds、writefds和exceptfds中的所有没有就绪的描述符。select的返回值有如下情况:
    正常情况下返回就绪的文件描述符个数;
    经过了timeout时长后仍无设备准备好,返回值为0;
    如果select被某个信号中断,它将返回-1并设置errno为EINTR。
    如果出错,返回-1并设置相应的errno。

    系统提供了4个宏对描述符集进行操作:
    #include <sys/select.h>
    #include <sys/time.h>
    void FD_SET(int fd, fd_set *fdset);
    void FD_CLR(int fd, fd_set *fdset);
    void FD_ISSET(int fd, fd_set *fdset);
    void FD_ZERO(fd_set *fdset);

    FD_SET    设置文件描述符集fdset中对应于文件描述符fd的位(设置为1)
    FD_CLR    清除文件描述符集fdset中对应于文件描述符fd的位(设置为 0)
    FD_ZERO   清除文件描述符集fdset中的所有位(既把所有位都设置为0)。
    使用这3个宏在调用select前设置描述符屏蔽位

    在调用select后使用
    FD_ISSET来检测文件描述符集fdset中对应于文件描述符fd的位是否被设置。

     select的中间三个指向描述符集的参数若全部为空指针,则select提供了较sleep(等待整数秒)更为精确的计时器。

poll系统调用
poll()系统调用是System V的多元I/O解决方案。它解决了select()的几个不足,尽管select()仍然经常使用
用户空间调用的poll函数定义如下:

#include
<sys/poll.h>
int poll (struct pollfd
*fds,
unsigned
int nfds,
int timeout);

和select()不一样,poll()没有使用低效的三个基于位的文件描述符set,而是采用了一个单独的结构体pollfd数组,由fds指针指向这个组。pollfd结构体定义如下:

#include
<sys/poll.h>

struct pollfd {
int fd;
/* file descriptor */

short events;
/* requested events to watch */
short revents;
/* returned events witnessed */
}; 

每一个pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码。内核在调用返回时设置这个域。events域中请求的任何事件都可能在revents域中返回。
合法的事件如下:
POLLIN                     有数据可读。
POLLRDNORM        有普通数据可读。
POLLRDBAND         有优先数据可读。
POLLPRI                  有紧迫数据可读。
POLLOUT                写数据不会导致阻塞。
POLLWRNORM       写普通数据不会导致阻塞。
POLLWRBAND        写优先数据不会导致阻塞。
POLLMSG                SIGPOLL消息可用。

此外,revents域中还可能返回下列事件:
POLLER                 指定的文件描述符发生错误。
POLLHUP              指定的文件描述符挂起事件。
POLLNVAL            指定的文件描述符非法。

timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。

成功时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:
EBADF
一个或多个结构体中指定的文件描述符无效。
EFAULT
fds指针指向的地址超出进程的地址空间。
EINTR
请求的事件之前产生一个信号,调用可以重新发起。
EINVAL
nfds参数超出PLIMIT_NOFILE值。
ENOMEM
可用内存不足,无法完成请求。

一个poll实现的样例代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/poll.h>
#define TIMEOUT 5       /* poll timeout, in seconds */
int main (void)
{
        struct pollfd fds[2];
        int ret;
        /* watch stdin for input */
        fds[0].fd = STDIN_FILENO;
        fds[0].events = POLLIN;
        /* watch stdout for ability to write (almost always true) */
        fds[1].fd = STDOUT_FILENO;
        fds[1].events = POLLOUT;
        /* All set, block! */
        ret = poll (fds, 2, TIMEOUT * 1000);
        if (ret == -1) {
                perror ("poll");
                return 1;
        }
        if (!ret) {
                printf ("%d seconds elapsed.\n", TIMEOUT);
                return 0;
        }
        if (fds[0].revents & POLLIN)
                printf ("stdin is readable\n");
        if (fds[1].revents & POLLOUT)
                printf ("stdout is writable\n");
        return 0;
}

epoll:

       接下来分析epoll,与poll/select不同,epoll不再是一个单独的系统调用,而是由epoll_create/epoll_ctl/epoll_wait三个系统调用组成,后面将会看到这样做的好处。

       先来看sys_epoll_create(epoll_create对应的内核函数),这个函数主要是做一些准备工作,比如创建数据结构,初始化数据并最终返回一个文件描述符(表示新创建的虚拟epoll文件),这个操作可以认为是一个固定时间的操作。


        epoll是做为一个虚拟文件系统来实现的,这样做至少有以下两个好处:

        1,可以在内核里维护一些信息,这些信息在多次epoll_wait间是保持的,比如所有受监控的文件描述符。

        2, epoll本身也可以被poll/epoll;

       具体epoll的虚拟文件系统的实现和性能分析无关,不再赘述。

       在sys_epoll_create中还能看到一个细节,就是epoll_create的参数size在现阶段是没有意义的,只要大于零就行。

       接着是sys_epoll_ctl(epoll_ctl对应的内核函数),需要明确的是每次调用sys_epoll_ctl只处理一个文件描述符,这里主要描述当op为EPOLL_CTL_ADD时的执行过程,sys_epoll_ctl做一些安全性检查后进入ep_insert,ep_insert里将 ep_poll_callback做为回掉函数加入设备的等待队列(假定这时设备尚未就绪),由于每次poll_ctl只操作一个文件描述符,因此也可以认为这是一个O(1)操作

        ep_poll_callback函数很关键,它在所等待的设备就绪后被系统回掉,执行两个操作:

       1,将就绪设备加入就绪队列,这一步避免了像poll那样在设备就绪后再次轮询所有设备找就绪者,降低了时间复杂度,由O(n)到O(1);   

       2,唤醒虚拟的epoll文件;

       最后是sys_epoll_wait,这里实际执行操作的是ep_poll函数。该函数等待将进程自身插入虚拟epoll文件的等待队列,直到被唤醒(见上面ep_poll_callback函数描述),最后执行ep_events_transfer将结果拷贝到用户空间。由于只拷贝就绪设备信息,所以这里的拷贝是一个O(1)操作。

epoll与select、poll区别

1、相比于select与poll,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。内核中的select与poll的实现是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。 

2、epoll的实现是基于回调的,如果fd有期望的事件发生就通过回调函数将其加入epoll就绪队列中,也就是说它只关心“活跃”的fd,与fd数目无关。 

3、内核 / 用户空间 内存拷贝问题,如何让内核把 fd消息通知给用户空间呢?在这个问题上select/poll采取了内存拷贝方法。而epoll采用了共享内存的方式。 

4、epoll不仅会告诉应用程序有I/0 事件到来,还会告诉应用程序相关的信息,这些信息是应用程序填充的,因此根据这些信息应用程序就能直接定位到事件,而不必遍历整个fd集合。

epoll 的EPOLLLT (水平触发,默认)和 EPOLLET(边沿触发)模式的区别

1、EPOLLLT:完全靠kernel epoll驱动,应用程序只需要处理从epoll_wait返回的fds,这些fds我们认为它们处于就绪状态。此时epoll可以认为是更快速的poll。

2、EPOLLET:此模式下,系统仅仅通知应用程序哪些fds变成了就绪状态,一旦fd变成就绪状态,epoll将不再关注这个fd的任何状态信息,(从epoll队列移除)直到应用程序通过读写操作(非阻塞)触发EAGAIN状态,epoll认为这个fd又变为空闲状态,那么epoll又重新关注这个fd的状态变化(重新加入epoll队列)。随着epoll_wait的返回,队列中的fds是在减少的,所以在大并发的系统中,EPOLLET更有优势,但是对程序员的要求也更高,因为有可能会出现数据读取不完整的问题,举例如下:

假设现在对方发送了2k的数据,而我们先读取了1k,然后这时调用了epoll_wait,如果是边沿触发,那么这个fd变成就绪状态就会从epoll 队列移除,很可能epoll_wait 会一直阻塞,忽略尚未读取的1k数据,与此同时对方还在等待着我们发送一个回复ack,表示已经接收到数据;如果是电平触发,那么epoll_wait 还会检测到可读事件而返回,我们可以继续读取剩下的1k 数据。

参考:

1. http://www.cnblogs.com/keanuyaoo/p/3275776.html

2.

http://www.cnblogs.com/mickole/articles/3204400.html

抱歉!评论已关闭.