1. 概述
poll、select、epoll是linux常用的多路IO复用的方法。其中poll和select的原理相似,本文以poll为例,而epoll在处理大量fd的时候,其效率和性能比poll和select好很多。本文将从源码的角度上上来分析poll和epoll的实现,从而说明epoll和poll性能差距的主要原因。
从个人观点来看,觉得poll和epoll的效率和性能上的差距,主要有以下两点:
1. 支持的fd的数量。poll监听的fd占用的是当前进程打开的fd,因此支持的fd的数量最大只能是FD_SETSIZE,一般为1024。epoll监听的fd是添加到epoll自己的红黑树上,能支持较多的fd,当然fd的数量和内存是相关的,比如1G的内存大概能支持10W个fd。
2. 效率。每次调用poll时,都需要将fd从用户空间copy到内核空间以及对每个fd调用filp->f_op->poll()函数,这样的代价比较大。而epoll在epoll_ctl()时,将fd从用户空间copy到内核空间,后续调用epoll_wait()时,不用再copy,另外,epoll也只是在epoll_ctl()增加fd的时候,需要调用filp->f_op->poll()一次。因此对比来讲,epoll()的效率比较高,特别是在监听大量fd的时候。
2. poll实现
接下来直接从poll()的系统调用开始,分析代码。分析的目的是说清楚poll的实现,因此下面的代码是精简过的,判断和检查逻辑都删掉了,如果大家有兴趣,可以下载完整的kernel源码,然后根据本文进行查阅。
fs/select.c
SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsigned int, nfds,
long, timeout_msecs)
{
struct timespec end_time, *to = NULL;
int ret;
//计算超时时间
if (timeout_msecs >= 0) {
to = &end_time;
poll_select_set_timeout(to, timeout_msecs / MSEC_PER_SEC,
NSEC_PER_MSEC * (timeout_msecs % MSEC_PER_SEC));
}
//主要函数
ret = do_sys_poll(ufds, nfds, to);
return ret;
}
poll() --> do_sys_poll():
int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,
struct timespec *end_time)
{
struct poll_wqueues table;
<span style="white-space:pre"> </span>//以下循环精简了
for (;;) {
//循环将每个pollfd从用户空间copy到内核空间,效率低
if (copy_from_user(walk->entries, ufds + nfds-todo,
sizeof(struct pollfd) * walk->len));
}
//初始化poll_wqueues:
// 1.设置poll_table的回调函数__pollwait()
// 2.记录当前进程current到poll_wqueues->polling_task。这样后面唤醒的时候,能找到该进程
poll_initwait(&table);
//对每个fd调用filp->f_op->poll()函数,将current添加到每个fd的等待队列上
fdcount = do_poll(nfds, head, &table, end_time);
//将current进程从每个fd的等待队列上移除
poll_freewait(&table);
<span style="white-space:pre"> </span>//此处精简了
return err;
}
do_sys_poll() --> poll_initwait():
void poll_initwait(struct poll_wqueues *pwq)
{
//给pwq->pt 设置回调函数__pollwait(),__pollwait()会在filp->f_op->poll()中被调用
init_poll_funcptr(&pwq->pt, __pollwait);
//将当前进程current记录到pwq->polling_task上,这样唤醒的时候,才能找到对应的进程
pwq->polling_task = current;
}
do_sys_poll() --> do_poll():
static int do_poll(unsigned int nfds, struct poll_list *list,
struct poll_wqueues *wait, struct timespec *end_time)
{
poll_table* pt = &wait->pt;
for (;;) {
struct poll_list *walk;
for (walk = list; walk != NULL; walk = walk->next) {
struct pollfd * pfd, * pfd_end;
pfd = walk->entries;
pfd_end = pfd + walk->len;
for (; pfd != pfd_end; pfd++) {
//循环对每个fd调用do_pollfd(),实际就是filp->f_op->poll()函数
if (do_pollfd(pfd, pt)) {
count++;
pt = NULL;
}
}
}
//如果有监听的时间发生或超时
if (count || timed_out)
break;
//设置超时时间,设置当前进程状态为TASK_INTERRUPTIBLE,然后让出cpu,等待唤醒或者超时。
if (!poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, to, slack))
timed_out = 1;
}
return count;
}
do_poll() --> do_pollfd():
static inline unsigned int do_pollfd(struct pollfd *pollfd, poll_table *pwait)
{
unsigned int mask;
int fd;
mask = 0;
fd = pollfd->fd;
if (fd >= 0) {
int fput_needed;
struct file * file;
//根据fd找到对应的file结构体,关于file结构体可以查看"linux VFS"中的描述
file = fget_light(fd, &fput_needed);
mask = POLLNVAL;
if (file != NULL) {
mask = DEFAULT_POLLMASK;
if (file->f_op && file->f_op->poll) {
if (pwait)
pwait->key = pollfd->events |
POLLERR | POLLHUP;
//就是这里了~,对每个fd调用file->f_op->poll()函数
//file是fd对应的文件结构体,file->f_op是文件的操作函数集,该调用关系在"linux VFS"中有讲述
mask = file->f_op->poll(file, pwait);
}
/* Mask out unneeded events. */
mask &= pollfd->events | POLLERR | POLLHUP;
fput_light(file, fput_needed);
}
}
pollfd->revents = mask;
return mask;
}
do_pollfd()中有关于VFS的一些知识,大家有兴趣可以参考
《linux VFS》。
f_op->poll()函数是每个支持poll()操作的文件系统或者驱动需要实现的&#