浅谈Select、Poll与Epoll的实现区别

本文关注内容:LinuxIO复用的三种机制,分别是Select、Poll以及Epoll。本文倾向于从浅谈三种机制的内核实现,来对比三种IO复用机制的性能差异。预备知识:等待队列和阻塞。

1 Select

1.1 Select API

1
2
#include <sys/select.h>
int select(int maxfdp1, fd_set *restrict readfds, fd_set* restrict writefds, fd_set* restrict exceptfds, struct timeval *restrict tvptr);

1.2 Select实现

  • 1.内核使用copy_from_user从用户空间拷贝fd_set到内核空间
  • 2.遍历所有关心的fd,如果fd就绪,就设置fd_set的值;如果fd不就绪,就将当前进程current加入到fd的等待队列(不休眠)。
  • 3.当遍历完所有的fd后,发现有fd就绪,则直接返回。如果没有一个是就绪的,那么调用schedule出让CPU进入睡眠(如果设置有超时时间,则调用schedule_timeout)。当有fd就绪的时候,会唤醒其等待队列上的进程。进程获得唤醒,再去重新遍历所有fd,判断是否有fd就绪。
  • 4.把fd_set从内核空间拷贝到用户空间。

1.3 Select的特点

  • 1.每次调用select都要重新设置fd_set。缺点:浪费时间,影响性能。
  • 2.调用select进入内核,或者从内核返回,都要拷贝一次fd集合。缺点:浪费时间,影响性能。
  • 3.文件描述符用fd集合组织,支持的数量大少,默认是1024。
  • 4.应用程序需要遍历整个fd_set,来获取哪些fd上的读写事件就绪。如果只有几个fd有就绪事件,遍历整个fd_set会造成极大的性能浪费。

2 Poll

2.1 Poll API

1
2
3
4
5
6
7
8
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd {
int fd;
short events;
short revents;
};

2.2 Poll实现

内核实现基本和Select相同,只是有一点不同。从poll()函数的参数可以看出。文件描述符改用pollfd结构的数组组织,数量就比select多得多。

2.3 Poll特点

    1. 调用poll进入内核或者从内核返回,内核都要拷贝一次pollfd结构数组。缺点:浪费时间。
    1. 应用程序需要遍历整个pollfd结构数组,才能获取哪些fd有就绪事件。

3 Epoll

Epoll由Select、Poll发展而来。从Select、Poll的缺点来看,我们可以总结一下几个优化点:

  • 1.Select、Poll每次调用都是一次独立的注册、收集、返回的过程,很低效。–> Epoll将注册感兴趣事件和收集就绪事件分开。
  • 2.Select、Poll每次需要拷贝事件集合fd_set或事件数组到内核,收集事件时再拷贝到用户空间。 –> Epoll对同一个事件,只注册一次。
  • 3.处理就绪事件,Select、Poll需要遍历整个fd_set或这pollfd结构数组。 –> Epoll收集事件时只返回就绪的事件。

3.1 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <sys/epoll.h> 

int epoll_create(int size); // 初始化一个epfd

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event); //添加、修改、删除感兴趣事件

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout); //一次事件收集

typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;

struct epoll_event {
uint32_t events;
epoll_data_t data;
};

3.2 Epoll实现

epoll_create分配一个文件描述符以及对应的struct file结构。分配一个struct eventpoll结构用体于管理,在该结构体初始化红黑树头节点,初始化就绪链表,初始化等待队列等等。这个eventpoll结构是epoll的关键,它管理这一棵红黑树,一个就绪链表,一个等待队列等等。

  • 红黑树:高效的插入和删除操作。用于在添加、修改、删除感兴趣事件。
  • 就绪链表:存放已经就绪的事件。epoll_wait直接从这里拷贝事件返回用户。
  • 等待队列:如果有多个进程等待在这个epfd上,则会加入到这个等待队列。

epoll_ctl+ add操作:分配一个与要监听的fd对应的epitem,并初始化相应的成员,然后插入到红黑树。并将当前进程current挂到对应fd的等待队列,并注册一个回调函数。该回调函数会在fd就绪时,将事件结构体加入到就绪队列中(这点有设备驱动触发中断完成)。epoll_ctl的删除操作和修改差不多。

epoll_wait直接查看就绪链表是否为空,如果不为空,则拷贝事件到用户态,然后返回。如果为空,则加入epfd的等待队列睡眠,等待被唤醒(如果设置超时的话,则要么超时醒来,要么有事件就绪,醒来)。进程醒过来,对就绪链表逐一拷贝到用户态。这里是先拷贝中间链表,在拷贝期间发生的事件仍然能够加入到就绪链表,等待下次epoll_wait收集。

3.3 ET和LT的实现

其实内核在实现ET和LT的时候非常简单。就是在拷贝就绪链表的时候,内核判断事件类型,如果是ET模式,则直接清除掉。如果是LT模式,只要相应的fd仍是就绪的,则重新加入就绪链表。

另外有一个细节:在注册感兴趣的事件的时候,内核就会检查fd是否就绪,如果就绪,则直接加入就绪链表(可见,就绪事件加入就绪链表不一定是设备驱动触发的)。

3.4 Epoll的优点

  • 使用红黑树管理监听的文件描述符,高效的插入、删除操作。
  • 事件只需注册一次。
  • 支持监听的文件描述符的数量非常大。
  • 每次返回的是就绪的fd数组。Select、Poll需要遍历整个所有描述符。

本文完。