zoukankan      html  css  js  c++  java
  • epoll 知识总结

     poll/select/epoll 对比

    http://www.cnblogs.com/apprentice89/p/3234677.html    ---有待继续学习

    http://blog.chinaunix.net/uid-20384806-id-1954307.html   ---有待继续学习

    select和poll即使只有一个描述符就绪,也要遍历整个集合。如果集合中活跃的描述符很少,遍历过程的开销就会变得很大,而如果集合中大部分的描述符都是活跃的,遍历过程的开销又可以忽略。

    epoll的实现中每次只遍历活跃的描述符(如果是水平触发,也会遍历先前活跃的描述符),在活跃描述符较少的情况下就会很有优势,在代码的分析过程中可以看到epoll的实现过于复杂并且其实现过程中需要同步处理(锁),如果大部分描述符都是活跃的,epoll的效率可能不如select或poll。(参见epoll 和poll的性能测试 http://jacquesmattheij.com/Poll+vs+Epoll+once+again)

    select能够处理的最大fd无法超出FDSETSIZE。

    select会复写传入的fd_set 指针,而poll对每个fd返回一个掩码,不更改原来的掩码,从而可以对同一个集合多次调用poll,而无需调整。

    select对每个文件描述符最多使用3个bit,而poll采用的pollfd需要使用64个bit,epoll采用的 epoll_event则需要96个bit

    如果事件需要循环处理select, poll 每一次的处理都要将全部的数据复制到内核,而epoll的实现中,内核将持久维护加入的描述符,减少了内核和用户复制数据的开销。

    所以,epoll的优势:

    1 如果事件需要循环处理,epoll只要拷贝文件描述符到kernel一次,而select/poll要拷贝多次。

    2 select会复写传入的fd_set 指针,而poll对每个fd返回一个掩码,不更改原来的掩码,从而可以对同一个集合多次调用poll,而无需调整。(select的fd_set既是输入又是输出,poll的输入输出分离)

    3 select和poll即使只有一个描述符就绪,也要遍历整个集合,而epoll的文件描述符都是就绪、有意义的,这点在集合中活跃的描述符很少的时候,epoll的优势明显

    4 epoll能突破select fd size 的最多限制。

    转自  http://www.zhihu.com/question/20122137

    2013-10-27更新:由于此文陆陆续续收到赞同,而且其中有些地方并不完全正确,特在本文最后予以订正

    我不了解楼主的层次,我必须从很多基础的概念开始构建这个答案,并且可能引申到很多别的问题。

    首先我们来定义流的概念,一个流可以是文件,socket,pipe等等可以进行I/O操作的内核对象。
    不管是文件,还是套接字,还是管道,我们都可以把他们看作流。
    之后我们来讨论I/O的操作,通过read,我们可以从流中读入数据;通过write,我们可以往流写入数据。现在假定一个情形,我们需要从流中读数据,但是流中还没有数据,(典型的例子为,客户端要从socket读如数据,但是服务器还没有把数据传回来),这时候该怎么办?

    • 阻塞。阻塞是个什么概念呢?比如某个时候你在等快递,但是你不知道快递什么时候过来,而且你没有别的事可以干(或者说接下来的事要等快递来了才能做);那么你可以去睡觉了,因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。
    • 非阻塞轮询。接着上面等快递的例子,如果用忙轮询的方法,那么你需要知道快递员的手机号,然后每分钟给他挂个电话:“你到了没?”

    很明显一般人不会用第二种做法,不仅显很无脑,浪费话费不说,还占用了快递员大量的时间。
    大部分程序也不会用第二种做法,因为第一种方法经济而简单,经济是指消耗很少的CPU时间,如果线程睡眠了,就掉出了系统的调度队列,暂时不会去瓜分CPU宝贵的时间片了。

    为了了解阻塞是如何进行的,我们来讨论缓冲区,以及内核缓冲区,最终把I/O事件解释清楚。缓冲区的引入是为了减少频繁I/O操作而引起频繁的系统调用(你知道它很慢的),当你操作一个流时,更多的是以缓冲区为单位进行操作,这是相对于用户空间而言。对于内核来说,也需要缓冲区。
    假设有一个管道,进程A为管道的写入方,B为管道的读出方。

    1. 假设一开始内核缓冲区是空的,B作为读出方,被阻塞着。然后首先A往管道写入,这时候内核缓冲区由空的状态变到非空状态,内核就会产生一个事件告诉B该醒来了,这个事件姑且称之为“缓冲区非空”。
    2. 但是“缓冲区非空”事件通知B后,B却还没有读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候,A写入的数据会滞留在内核缓冲区中,如果内核也缓冲区满了,B仍未开始读数据,最终内核缓冲区会被填满,这个时候会产生一个I/O事件,告诉进程A,你该等等(阻塞)了,我们把这个事件定义为“缓冲区满”。
    3. 假设后来B终于开始读数据了,于是内核的缓冲区空了出来,这时候内核会告诉A,内核缓冲区有空位了,你可以从长眠中醒来了,继续写数据了,我们把这个事件叫做“缓冲区非满”
    4. 也许事件Y1已经通知了A,但是A也没有数据写入了,而B继续读出数据,知道内核缓冲区空了。这个时候内核就告诉B,你需要阻塞了!,我们把这个时间定为“缓冲区空”。

    这四个情形涵盖了四个I/O事件,缓冲区满,缓冲区空,缓冲区非空,缓冲区非满(注都是说的内核缓冲区,且这四个术语都是我生造的,仅为解释其原理而造)。这四个I/O事件是进行阻塞同步的根本。(如果不能理解“同步”是什么概念,请学习操作系统的锁,信号量,条件变量等任务同步方面的相关知识)。

    然后我们来说说阻塞I/O的缺点。但是阻塞I/O模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。
    于是再来考虑非阻塞忙轮询的I/O方式,我们发现我们可以同时处理多个流了(把一个流从阻塞模式切换到非阻塞模式再此不予讨论):
    while true {
    for i in stream[]; {
    if i has data
    read until unavailable
    }
    }
    我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为如果所有的流都没有数据,那么只会白白浪费CPU。这里要补充一点,阻塞模式下,内核对于I/O事件的处理是阻塞或者唤醒,而非阻塞模式下则把I/O事件交给其他对象(后文介绍的select以及epoll)处理甚至直接忽略。

    为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样的)。这个代理比较厉害,可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可以把“忙”字去掉了)。代码长这样:
    while true {
    select(streams[])
    for i in streams[] {
    if i has data
    read until unavailable
    }
    }
    于是,如果没有I/O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。
    但是使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,每一次无差别轮询时间就越长。再次
    说了这么多,终于能好好解释epoll了
    epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(k),k为产生I/O事件的流的个数,也有认为O(1)的[更新 1])
    在讨论epoll的实现细节之前,先把epoll的相关操作列出[更新 2]:

    • epoll_create 创建一个epoll对象,一般epollfd = epoll_create()
    • epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件
      比如
      epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//有缓冲区内有数据时epoll_wait返回
      epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//缓冲区可写入时epoll_wait返回
    • epoll_wait(epollfd,...)等待直到注册的事件发生

    (注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。而epoll只关心缓冲区非满和缓冲区非空事件)。
    一个epoll模式的代码大概的样子是:
    while true {
    active_stream[] = epoll_wait(epollfd)
    for i in active_stream[] {
    read or write till unavailable
    }
    }
    限于篇幅,我只说这么多,以揭示原理性的东西,至于epoll的使用细节,请参考man和google,实现细节,请参阅linux kernel source。
    ======================================
    [更新1]: 原文为O(1),但实际上O(k)更为准确
    [更新2]: 原文所列第二点说法让人产生EPOLLIN/EPOLLOUT等同于“缓冲区非空”和“缓冲区非满”的事件,但并非如此,详细可以Google关于epoll的边缘触发和水平触发。

    首先说一下 ET 和 LT。ET 和 LT 这两个术语实际上并不仅仅使用在 epoll 上(甚至被用于其他领域),它们的含义为:
    LT:在特定状态下触发
    ET:在状态变化时触发

    网络 IO 管理中,告知逻辑层文件描述符可读和可写时,可以是在文件描述符处于可读和可写状态时(LT),也可以是文件描述符变为可读和可写状态时(ET)。

    以可读为例,我们可以理解,在 ET 下,当出现数据可读,那么会通知逻辑层一次,如果逻辑层这次通知时没有完全读取完数据,那么不会再得到底层通知。相比下,LT 下,逻辑层会一直得到底层通知,直到文件描述符状态为不可读。

    由于上面的情况,在 ET 下,文件描述符就不能是阻塞的,因为逻辑层需要在一次通知时不断读取数据,直到所有数据读取完成,读取操作是不能被阻塞的。(换句话说,如果socket/文件描述符是阻塞的,那么在没有数据的时候就会阻塞,而不是返回EAGAIN,那么程序就会一直阻塞到下一次数据了,然后又读完,然后又阻塞,变成了死循环。。)

    select,poll,epoll LT(可以认为是快速的 poll)都是 LT 的,所以文件描述符可以为阻塞的(当然也可以为非阻塞的)

    epoll ET 文件描述符必须是非阻塞的

    LT 表示「有东西等着我就告诉你」,这时候你就算怕阻塞一次没读完,转一圈回我这儿来还能继续。你要是不断用 non-blocking 去读到啥都不剩呢也挺好,省我事了。
    ET 表示「有东西新来了我吼一声你听见听不见听见了取不取是你的事情」,这样你要是一次没取完,下次来新货之前这些东西就烂在这里了,所以你必须重复去试着读——然后如果你的 socket 不是 non-blocking 的,恭喜……

    http://www.cppblog.com/feixuwu/archive/2010/07/10/119995.html

    最近有朋友在面试的时候被问了select 和epoll效率差的原因,和一般人一样,大部分都会回答select是轮询、epoll是触发式的,所以效率高。这个答案听上去很完美,大致也说出了二者的主要区别。
    今天闲来无事,翻看了下内核代码,结合内核代码和大家分享下我的观点。

    一、连接数

    我本人也曾经在项目中用过select和epoll,对于select,感触最深的是linux下select最大数目限制(windows 下似乎没有限制),每个进程的select最多能处理FD_SETSIZE个FD(文件句柄),
    如果要处理超过1024个句柄,只能采用多进程了。
    常见的使用slect的多进程模型是这样的: 一个进程专门accept,成功后将fd通过unix socket传递给子进程处理,父进程可以根据子进程负载分派。曾经用过1个父进程+4个子进程 承载了超过4000个的负载。
    这种模型在我们当时的业务运行的非常好。epoll在连接数方面没有限制,当然可能需要用户调用API重现设置进程的资源限制。

    二、IO差别

    1、select的实现

    这段可以结合linux内核代码描述了,我使用的是2.6.28,其他2.6的代码应该差不多吧。
    先看看select:
    select系统调用的代码在fs/Select.c下,
    asmlinkage long sys_select(int n, fd_set __user *inp, fd_set __user *outp,
                fd_set __user *exp, struct timeval __user *tvp)
    {
        struct timespec end_time, *to = NULL;
        struct timeval tv;
        int ret;

        if (tvp) {
            if (copy_from_user(&tv, tvp, sizeof(tv)))
                return -EFAULT;

            to = &end_time;
            if (poll_select_set_timeout(to,
                    tv.tv_sec + (tv.tv_usec / USEC_PER_SEC),
                    (tv.tv_usec % USEC_PER_SEC) * NSEC_PER_USEC))
                return -EINVAL;
        }

        ret = core_sys_select(n, inp, outp, exp, to);
        ret = poll_select_copy_remaining(&end_time, tvp, 1, ret);

        return ret;

    前面是从用户控件拷贝各个fd_set到内核空间,接下来的具体工作在core_sys_select中,
    core_sys_select->do_select,真正的核心内容在do_select里:
    int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
    {
        ktime_t expire, *to = NULL;
        struct poll_wqueues table;
        poll_table *wait;
        int retval, i, timed_out = 0;
        unsigned long slack = 0;

        rcu_read_lock();
        retval = max_select_fd(n, fds);
        rcu_read_unlock();

        if (retval < 0)
            return retval;
        n = retval;

        poll_initwait(&table);
        wait = &table.pt;
        if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
            wait = NULL;
            timed_out = 1;
        }

        if (end_time && !timed_out)
            slack = estimate_accuracy(end_time);

        retval = 0;
        for (;;) {
            unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;

            set_current_state(TASK_INTERRUPTIBLE);

            inp = fds->in; outp = fds->out; exp = fds->ex;
            rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;

            for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
                unsigned long in, out, ex, all_bits, bit = 1, mask, j;
                unsigned long res_in = 0, res_out = 0, res_ex = 0;
                const struct file_operations *f_op = NULL;
                struct file *file = NULL;

                in = *inp++; out = *outp++; ex = *exp++;
                all_bits = in | out | ex;
                if (all_bits == 0) {
                    i += __NFDBITS;
                    continue;
                }

                for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
                    int fput_needed;
                    if (i >= n)
                        break;
                    if (!(bit & all_bits))
                        continue;
                    file = fget_light(i, &fput_needed);
                    if (file) {
                        f_op = file->f_op;
                        mask = DEFAULT_POLLMASK;
                        if (f_op && f_op->poll)
                            mask = (*f_op->poll)(file, retval ? NULL : wait);
                        fput_light(file, fput_needed);
                        if ((mask & POLLIN_SET) && (in & bit)) {
                            res_in |= bit;
                            retval++;
                        }
                        if ((mask & POLLOUT_SET) && (out & bit)) {
                            res_out |= bit;
                            retval++;
                        }
                        if ((mask & POLLEX_SET) && (ex & bit)) {
                            res_ex |= bit;
                            retval++;
                        }
                    }
                }
                if (res_in)
                    *rinp = res_in;
                if (res_out)
                    *routp = res_out;
                if (res_ex)
                    *rexp = res_ex;
                cond_resched();
            }
            wait = NULL;
            if (retval || timed_out || signal_pending(current))
                break;
            if (table.error) {
                retval = table.error;
                break;
            }

            /*
             * If this is the first loop and we have a timeout
             * given, then we convert to ktime_t and set the to
             * pointer to the expiry value.
             */
            if (end_time && !to) {
                expire = timespec_to_ktime(*end_time);
                to = &expire;
            }

            if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
                timed_out = 1;
        }
        __set_current_state(TASK_RUNNING);

        poll_freewait(&table);

        return retval;

    上面的代码很多,其实真正关键的代码是这一句:
    mask = (*f_op->poll)(file, retval ? NULL : wait); 
    这个是调用文件系统的 poll函数,不同的文件系统poll函数自然不同,由于我们这里关注的是tcp连接,而socketfs的注册在 net/Socket.c里。
    register_filesystem(&sock_fs_type); 
    socket文件系统的函数也是在net/Socket.c里:
    static const struct file_operations socket_file_ops = {
        .owner =    THIS_MODULE,
        .llseek =    no_llseek,
        .aio_read =    sock_aio_read,
        .aio_write =    sock_aio_write,
        .poll =        sock_poll,
        .unlocked_ioctl = sock_ioctl,
    #ifdef CONFIG_COMPAT
        .compat_ioctl = compat_sock_ioctl,
    #endif
        .mmap =        sock_mmap,
        .open =        sock_no_open,    /* special open code to disallow open via /proc */
        .release =    sock_close,
        .fasync =    sock_fasync,
        .sendpage =    sock_sendpage,
        .splice_write = generic_splice_sendpage,
        .splice_read =    sock_splice_read,
    };
    从sock_poll跟随下去,
    最后可以到 net/ipv4/tcp.c的
    unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait) 
    这个是最终的查询函数,
    也就是说select 的核心功能是调用tcp文件系统的poll函数,不停的查询,如果没有想要的数据,主动执行一次调度(防止一直占用cpu),直到有一个连接有想要的消息为止。
    从这里可以看出select的执行方式基本就是不同的调用poll,直到有需要的消息为止,如果select 处理的socket很多,这其实对整个机器的性能也是一个消耗。

    2、epoll的实现

    epoll的实现代码在 fs/EventPoll.c下,
    由于epoll涉及到几个系统调用,这里不逐个分析了,仅仅分析几个关键点,
    第一个关键点在
    static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
                 struct file *tfile, int fd) 
    这是在我们调用sys_epoll_ctl 添加一个被管理socket的时候调用的函数,关键的几行如下:
    epq.epi = epi;
        init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);

        /*
         * Attach the item to the poll hooks and get current event bits.
         * We can safely use the file* here because its usage count has
         * been increased by the caller of this function. Note that after
         * this operation completes, the poll callback can start hitting
         * the new item.
         */
        revents = tfile->f_op->poll(tfile, &epq.pt); 
    这里也是调用文件系统的poll函数,不过这次初始化了一个结构,这个结构会带有一个poll函数的callback函数:ep_ptable_queue_proc,
    在调用poll函数的时候,会执行这个callback,这个callback的功能就是将当前进程添加到 socket的等待进程上。
    static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
                     poll_table *pt)
    {
        struct epitem *epi = ep_item_from_epqueue(pt);
        struct eppoll_entry *pwq;

        if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
            init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
            pwq->whead = whead;
            pwq->base = epi;
            add_wait_queue(whead, &pwq->wait);
            list_add_tail(&pwq->llink, &epi->pwqlist);
            epi->nwait++;
        } else {
            /* We have to signal that an error occurred */
            epi->nwait = -1;
        }
    }  
    注意到参数 whead 实际上是 sk->sleep,其实就是将当前进程添加到sk的等待队列里,当该socket收到数据或者其他事件触发时,会调用
    sock_def_readable 或者sock_def_write_space 通知函数来唤醒等待进程,这2个函数都是在socket创建的时候填充在sk结构里的。
    从前面的分析来看,epoll确实是比select聪明的多、轻松的多,不用再苦哈哈的去轮询了。

    http://blog.csdn.net/will130/article/details/51072819 

    select,poll,epoll都是IO多路复用的机制I/O多路复用就是通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作

    select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

    I/O复用模型会用到select、poll、epoll函数:对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性。但关键是能实现同时对多个IO端口进行监听

    这几个函数也会使进程阻塞,但是和阻塞I/O所不同的是,这几个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。

    1、select

    1.1 select进行IO复用原理

    当一个客户端连接上服务器时,服务器就将其连接的fd加入fd_set集合,等到这个连接准备好读或写的时候,就通知程序进行IO操作,与客户端进行数据通信。!!!

    大部分 Unix/Linux 都支持 select 函数,该函数用于探测多个文件描述符的状态变化。

    1.2 select函数原型

    int select(  
         int maxfdp, //Winsock中此参数无意义  
         fd_set* readfds, //进行可读检测的Socket  
         fd_set* writefds, //进行可写检测的Socket  
         fd_set* exceptfds, //进行异常检测的Socket  
         const struct timeval* timeout //非阻塞模式中设置最大等待时间  
    )  

    1.3 使用select的步骤

    1)创建所关注的事件的描述符集合(fd_set),对于一个描述符,可以关注其上面的读(read)、写(write)、异常(exception)事件,所以通常,要创建三个fd_set,一个用来收集关注读事件的描述符,一个用来收集关注写事件的描述符,另外一个用来收集关注异常事件的描述符集合。

    2)调用select()等待事件发生。这里需要注意的一点是,select的阻塞与是否设置非阻塞I/O是没有关系的。

    3)轮询所有fd_set中的每一个fd,检查是否有相应的事件发生,如果有,就进行处理。

    1.4 select的优缺点

    相比其他模型,使用 select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。

    select的缺点:

    (1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大!!!(复制大量句柄数据结构,产生巨大的开销 )。

    (2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大!!!(消耗大量时间去轮询各个句柄,才能发现哪些句柄发生了事件)。

    (3)单个进程能够监视的文件描述符的数量存在最大限制,32位机默认是1024。

    (4)select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

    (5)该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。

    2、poll

    poll库是在linux2.1.23中引入的,windows平台不支持poll。poll本质上和select没有太大区别,都是先创建一个关注事件的描述符的集合,然后再去等待这些事件发生,然后再轮询描述符集合,检查有没有事件发生,如果有,就进行处理。

    因此,poll有着与select相似的处理流程:

    1)创建描述符集合,设置关注的事件
    2)调用poll(),等待事件发生。下面是poll的原型:
            int poll(struct pollfd *fds, nfds_t nfds, int timeout);
            类似select,poll也可以设置等待时间,效果与select一样。
    3)轮询描述符集合,检查事件,处理事件。

    poll与select的主要区别在于:select需要为读、写、异常事件分别创建一个描述符集合,最后轮询的时候,需要分别轮询这三个集合。而poll只需要一个集合,在每个描述符对应的结构上分别设置读、写、异常事件,最后轮询的时候,可以同时检查三种事件。

    它没有最大连接数的限制,原因是它是基于链表来存储的。

    缺点:

    1)大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。 
    2)poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

    3、epoll

    3.1 epoll概述

    poll和select,它们的最大的问题就在于效率。它们的处理方式都是创建一个事件列表,然后把这个列表发给内核,返回的时候,再去轮询检查这个列表,这样在描述符比较多的应用中,效率就显得比较低下了。

    epoll是一种比较好的做法,它把描述符列表交给内核,一旦有事件发生,内核把发生事件的描述符列表通知给进程,这样就避免了轮询整个描述符列表。

    epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

    epoll与select和poll的调用接口上的不同:select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

    3.2 epoll的使用步骤

    1)创建一个epoll描述符,调用epoll_create()来完成。epoll_create()有一个整型的参数size,用来告诉内核,要创建一个有size个描述符的事件列表(集合)。

    int epoll_create(int size)

    2)给描述符设置所关注的事件,并把它添加到内核的事件列表中。这里需要调用epoll_ctl()来完成。

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

    3)等待内核通知事件发生,得到发生事件的描述符的结构列表。该过程由epoll_wait()完成。得到事件列表后,就可以进行事件处理了。

    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)

    3.3 epoll的LT和ET的区别

    水平触发和边缘触发的区别:只要句柄满足某种状态,水平触发就会发出通知;而只有当句柄状态改变时,边缘触发才会发出通知。

    LT:水平触发效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。

    ET:边缘触发效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。

    3.4 epoll的优点

    1)没有最大并发连接的限制,能打开FD的上限远大于1024(1G的内存上能监听约10万个端口);

    2)效率提升。不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;

    即epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll

    3)内存拷贝epoll通过内核和用户空间共享一块内存来实现消息传递的。利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap 减少复制开销epoll保证了每个fd在整个过程中只会拷贝一次(select,poll每次调用都要把fd集合从用户态往内核态拷贝一次)。

    转自 
    http://www.cnblogs.com/Anker/p/3265058.html 
    http://blog.csdn.net/hguisu/article/details/7453390 
    http://blog.csdn.net/hguisu/article/details/38638183#t5

    5、例题:

    1、关于epoll和select的区别,哪些说法是正确的? 
    正确答案:A B C

    A、epoll和select都是I/O多路复用的技术,都可以实现同时监听多个I/O事件的状态 
    B、epoll相比select效率更高,主要是基于其操作系统支持的I/O事件通知机制,而select是基于轮询机制 
    C、epoll支持水平触发和边沿触发两种模式 
    D、select能并行支持I/O比较小,且无法修改

  • 相关阅读:
    vue element-ui,上传文件加载进度条显示效果(使用定时器实现源码分享)
    vue element-ui 上传文件的 :on-progress钩子无法触发的原因及报错原因
    vue打包文件后首次加载速度慢解决方法----1.压缩文件js.css 2.使用cdn加载
    vue 报错 RangeError: Maximum call stack size exceeded
    vue在IE11报错‘vuex requires a Promise polyfill in this browser.’
    轻松理解MYSQL MVCC 实现机制
    推荐:mysql锁 innodb下的记录锁,间隙锁,next-key锁
    MySQL的四种事务隔离级别
    php中文件上传大小限制如何修改
    Https原理及流程
  • 原文地址:https://www.cnblogs.com/diegodu/p/4602849.html
Copyright © 2011-2022 走看看