select/poll/epoll分析:區別與聯繫

IO模型中一個重要的多路處理模型。

背景:

程序需要處理多路IO時,靠阻塞的同步IO或者非阻塞的輪詢都不是太好的選擇。

因爲阻塞IO只能處理單路IO比較有效,而非阻塞的輪詢無論是否有IO到來都會形成開銷。

因此需要一種事件推動的模型,能對多路IO的就緒狀態進行監聽。類型於硬件中斷驅動機制。

select/poll/epoll便於用於這個目的。

比較:

  特點 問題點
select 用數組的方式指定監聽的多路IO 有最大監聽數量的限制,最大1024
poll 用鏈表來指定監聽的多路IO

1.解決了select監聽數量限制

2.用戶需要遍歷所有的IO,才能找到就緒的IO,開銷是O(n).

3.每次拷貝要監聽的IO數據開銷。

4.內核也要遍歷所有的IO,才能找到就緒的IO,開銷是O(n)

epoll 監聽IO集合單獨指定,返回就緒的IO集合

1.去掉了不必要的多次拷貝要監聽的IO數據開銷。

2.解決了遍歷所有的IO的0(n)開銷。有就緒的IO就單獨通過回調函數把自己加入就緒IO集合中。

從上表看起來,一個比一個要好。epoll似乎是最優美的,沒有任何冗餘的操作與不必要的限制。

具體內核代碼分析:

fs/select.c
int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
...

for (;;) {

...
    for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1) {  遍歷所有的IO
    if (f_op && f_op->poll) {
                        wait_key_set(wait, in, out,
                                 bit, busy_flag);
                        mask = (*f_op->poll)(f.file, wait);
                    }    
    }
    if (retval || timed_out || signal_pending(current))
            break;
        if (table.error) {
            retval = table.error;
            break;
        }
    if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
                       to, slack))
            timed_out = 1;

}

}

static int do_poll(unsigned int nfds,  struct poll_list *list,
           struct poll_wqueues *wait, struct timespec *end_time)
{

...

for (;;) {
...        for (walk = list; walk != NULL; walk = walk->next) { 遍歷所有的IO
          struct pollfd * pfd, * pfd_end;

            pfd = walk->entries;
            pfd_end = pfd + walk->len;
            for (; pfd != pfd_end; pfd++) {            
                if (do_pollfd(pfd, pt, &can_busy_loop,
                          busy_flag)) {
                    count++;
                    pt->_qproc = NULL;
                    /* found something, stop busy polling */
                    busy_flag = 0;
                    can_busy_loop = false;
                }
            }
        }
      

pt->_qproc = NULL;
        if (!count) {
            count = wait->error;
            if (signal_pending(current))
                count = -EINTR;
        }
        if (count || timed_out)
            break;

        /* only if found POLL_BUSY_LOOP sockets && not out of time */
        if (can_busy_loop && !need_resched()) {
            if (!busy_end) {
                busy_end = busy_loop_end_time();
                continue;
            }
            if (!busy_loop_timeout(busy_end))
                continue;
        }
        busy_flag = 0;       
        if (end_time && !to) {
            expire = timespec_to_ktime(*end_time);
            to = &expire;
        }

        if (!poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, to, slack))
            timed_out = 1;
    }

}

fs/eventpoll.c

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
           int maxevents, long timeout)

{

fetch_events:
    spin_lock_irqsave(&ep->lock, flags);

    if (!ep_events_available(ep)) { //判斷是否有就緒IO

      for (;;) {
   
            set_current_state(TASK_INTERRUPTIBLE);
            if (ep_events_available(ep) || timed_out) //判斷是否有就緒IO
                break;
            if (signal_pending(current)) {
                res = -EINTR;
                break;
            }

            spin_unlock_irqrestore(&ep->lock, flags);
            if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
                timed_out = 1;

            spin_lock_irqsave(&ep->lock, flags);
        }

}

static inline int ep_events_available(struct eventpoll *ep)
{
    return !list_empty(&ep->rdllist) || ep->ovflist != EP_UNACTIVE_PTR; //判斷是否有就緒IO,是看rdlist是否爲空或者有異常
}

在add 新的epoll時會將wakeup的默認處理回調設置爲自定義的ep_poll_callback

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;
        if (epi->event.events & EPOLLEXCLUSIVE)
            add_wait_queue_exclusive(whead, &pwq->wait);
        else
            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;
    }
}

接下來,肯定不會猜錯,會在回調中把就緒IO加入rdlist.

static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
...

    /* If this file is already in the ready list we exit soon */
    if (!ep_is_linked(&epi->rdllink)) {
        list_add_tail(&epi->rdllink, &ep->rdllist);
        ep_pm_stay_awake_rcu(epi);
    }

...

}

select/poll幾乎是一樣的,只是接口方式有些不一樣,運行原理上相同,而epoll在運行原理上是不同的。

關於驅動:

驅動實現就比較簡單了,因爲它只是框架的一部分,就是把調用  poll_wait()把自己放入隊列,並返回事件信息,然後在事情發生時調用wakeup().

樣例:

static unsigned int bt_bmc_poll(struct file *file, poll_table *wait)
{
    struct bt_bmc *bt_bmc = file_bt_bmc(file);
    unsigned int mask = 0;
    u8 ctrl;

    poll_wait(file, &bt_bmc->queue, wait);

    ctrl = bt_inb(bt_bmc, BT_CTRL);

    if (ctrl & BT_CTRL_H2B_ATN)
        mask |= POLLIN;

    if (!(ctrl & (BT_CTRL_H_BUSY | BT_CTRL_B2H_ATN)))
        mask |= POLLOUT;

    return mask;
}

static irqreturn_t bt_bmc_irq(int irq, void *arg)
{
    struct bt_bmc *bt_bmc = arg;
    u32 reg;

    reg = ioread32(bt_bmc->base + BT_CR2);
    reg &= BT_CR2_IRQ_H2B | BT_CR2_IRQ_HBUSY;
    if (!reg)
        return IRQ_NONE;

    /* ack pending IRQs */
    iowrite32(reg, bt_bmc->base + BT_CR2);

    wake_up(&bt_bmc->queue);
    return IRQ_HANDLED;
}

 

總結:

1.所以說epoll真的是event事件驅動,O(1)的效率。select/poll是遍歷找到事件在哪裏。

2. 但是此處在性能上又有類似中斷與polling的特點。在大量事件產生時,interrupt的處理流程開銷必然不變,同時對錶的操作要加lock機制上又有開銷,造成單個事件處理開銷並要比select/poll要高。在數量起來後,反而在性能上epoll不一定比select/poll好。
因此:看情況來選擇,而不是epoll萬能適用。對於少量的多路IO其實都還是可以的,不用太糾結哪個一定好。對於大數量的多路,而事件不太多的情況倒是最適用的。

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