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其实都还是可以的,不用太纠结哪个一定好。对于大数量的多路,而事件不太多的情况倒是最适用的。