IO多路复用浅析(Epoll、Poll、 Select)
IO多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步IO的实现会负责把数据从内核拷贝到用户空间。
1 2 3 查看man手册 man pagenum function //查看对应的函数原型, pagenum 为页数, function 为函数名 如: man 2 select
Select 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) ; struct timeval { long tv_sec; long tv_usec; }; void FD_CLR (int fd, fd_set *set) ; int FD_ISSET (int fd, fd_set *set) ; void FD_SET (int fd, fd_set *set) ; void FD_ZERO (fd_set *set) ;
调用过程如下: fd_set如下所示: types.sh
1 2 3 4 5 6 7 8 9 10 typedef long fd_mask; typedef struct _types_fd_set { fd_mask fds_bits[howmany(FD_SETSIZE, NFDBITS)]; } _types_fd_set;
fd_set就是long的数组,数组的大小取决于FD_SETSIZE/NFDBITS,如果1个long是8字节64位的话,fds_bits的大小就是16
默认最大监听描述符个数为1024;但是可以通过内核编译来重设FD_SETSIZE,并且在linux下是FD_SETSIZE限制的是值,文件描述符值的大小不能大于1024
每次调用时都需要将描述符和事件从用户空间拷贝到内核空间(copy_from_user)
查询事件时采用遍历轮询的方式,select函数会修改readfds,writefds,exceptfds,如果一个文件描述符就绪,则为1,没就绪则为0,这样调用者要遍历整个位域,通过要通过FD_ISSET宏来找到就绪的文件描述符。时间复杂度 O(n)。
1 2 ulimit -n //查看当前最大监听描述符设置ulimit -n size //size:具体的值的大小,设置更改
Poll 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <poll.h> int poll (struct pollfd *fds, nfds_t nfds, int timeout) ;struct pollfd { int fd; short events; short revents; };
不同于select,最大监听描述符不设限制(也不是指可以无限大),poll没有用fd_set数据类型,而是使用了pollfd数组(事件基于链表的数据结构存储)
查询事件时采用遍历轮询的方式,时间复杂度 O(n)
监听、返回集合分离
水平触发,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
Epoll 1 2 #include <sys/epoll.h> int epoll_create (int size)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <sys/epoll.h> int epoll_ctl (int epfd, int op, int fd, struct epoll_event *event) struct epoll_event { __uint32_t events; epoll_data_t data; }; typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t ;
1 2 3 4 5 6 7 8 9 #include <sys/epoll.h> int epoll_wait (int epfd, struct epoll_event *events, int maxevents, int timeout)
无最大监听描述符个数限制(并非无限大)1 通过 cat /proc/sys/fs/file-max 查看
底层基于红黑树和就绪链表,IO的效率不会随着监视fd的数量的增长而下降。不同于之前的轮询,而是通过每个fd定义的回调函数来实现。只有就绪的fd才会执行回调函数。时间复杂度O(1)
红黑树存储所监控的文件描述符的节点数据,就绪链表存储就绪的文件描述符的节点数据;epoll_ctl将会添加新的描述符,首先判断是红黑树上是否有此文件描述符节点,如果有,则立即返回。如果没有, 则在树干上插入新的节点,并且告知内核注册回调函数。当接收到某个文件描述符过来数据时,那么内核将该节点插入到就绪链表里面。epoll_wait将会接收到消息,并且将数据拷贝到用户空间,清空链表。 获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合
提供边沿触发ET模式,效率高于LT
总结
在描述符不是太多的情况下,且大部分描述符处于活跃状态,select, poll仍然是不错的选择,几乎所有的平台都有对他们的实现提供接口,便于移植。
epoll的底层依然需要设备驱动提供poll回调来作为状态检测基础,但通过 epoll_ctl的EPOLL_CTL_ADD命令把描述符添加进epoll内部管理器里,只需添加一次即可,直到用 epoll_ctl的EPOLL_CTL_DEL命令删除此描述符为止,而不像select/poll是每次执行都必须添加,很显然大量减少了描述符在内核和用户空间不断的来回copy的开销。
参考链接 Linux IO模式及 select、poll、epoll详解 深入理解select、poll和epoll及区别 为什么select打开的FD数量有限制,而poll、epoll等打开的FD数量没有限制? socket编程以及select、epoll、poll示例详解 linux内核select/poll,epoll实现与区别