缘起
面试的时候经常被问的一个很蛋疼的问题,经常被问,但是知识很零散,难记忆,看完就忘
select
作用
可以监视文件描述符是否可以读写,要求监视的文件描述符是非阻塞的
诞生背景
产生与上个世纪80年代的UNIX系统,到1993年写入POSIX1.b规范(一个操作系统的编程接口的规范,你要是写个操作系统想被兼容得遵守这个规范)。由于那个年代还没有多线程(2年后线程相关的内容才写入POSIX1.c规范),还没有什么C10K问题,所以在设计select的时候体现了那个年代的特点。
接口
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds 是参数2,3,4中最大的文件描述符 + 1
readfds 是要检测fd的可读事件,当fd可读的时候select就返回
writefds 是要检测fd的写事件,当fd可写的时候select就返回
excpetfds 是要检测fd的出错事件,当fd出错的时候select就返回,当是NULL的时候,就是检测readfds和writefds的出错事件,所以一般都写NULL
timeout 是一个纳秒级的超时时间
想使用这个函数需要设置个fd_set结构,所以就用到void FD_SET(int fd, fd_set *set);这个宏,用来设置fd_set
使用过程
fd_set fd_in, fd_out; struct timeval tv; // 初始化fd_set FD_ZERO( &fd_in ); FD_ZERO( &fd_out ); // 把网络IO复制到fd_set FD_SET( sock1, &fd_in ); FD_SET( sock1, &fd_out ); // 用户初始化select int largest_sock = sock1 > sock2 ? sock1 : sock2; // 设置select的超时时间 tv.tv_sec = 10; tv.tv_usec = 0; // 调用select 并阻塞在这里等待 IO事件 int ret = select( largest_sock, &fd_in, &fd_out, NULL, &tv ); // 检查返回值的状态 if ( ret == -1 ) // 异常情况 else if ( ret == 0 ) // 超时或者没有可以监控的fd else { // 检测每个IO事件是否可以读写 if ( FD_ISSET( sock1, &fd_in ) ) // IO可读 if ( FD_ISSET( sock2, &fd_out ) ) // IO可写 }
可以看到使用select的时候,每个fd对应一个fd_set结构,然后调用FD_SET,调用select以后进入polling,等返回以后通过FD_ISSET对每个fd_set检测是否可读可写。
存在问题
是不是会儿还没有现在nginx几万并发的场景,select只能对1024个fd进行监控
select 函数会修改fd_set,所以每次调用完select以后需要重新通过FD_SET设置fd_set
select 返回以后并不知道具体哪个fd可以读写,需要使用FD_ISSET把所有的fd检测一遍才知道具体是哪个可读可写
那年代估计不像现在这么广泛的用多线程,所以select中的fd_set在调用select的时候相当于被独占的
优点
使用简单,POSIX标准所以跨平台比较好
POLL
功能和select相同,但是主要解决select的一些限制
接口
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
fds 是一个pollfd的数组,和select的fd_set差不多,下面具体解释
nfds 是fds数组的长度,可以看到没有select还需要求一个fd最大值再加1那么麻烦
timeout 是超时的毫秒数
pollfd的结构
struct pollfd { int fd; /* 文件描述符 */ short events; /* 需要监听的事件 */ short revents; /* 返回的事件 */ };
对比一下select,接口上更加优雅,首先,pollfd 通过单独的events来区分了监控的是什么样的事件,而不是像select那样通过参数来区分。
使用过程
// 创建pollfd struct pollfd fds[2]; // 设置pollfd的fd和要监控的事件,sock1监控读,sock2监控写 fds[0].fd = sock1; fds[0].events = POLLIN; fds[1].fd = sock2; fds[1].events = POLLOUT; // 10秒超时,开始等待sock1和sock2上的事件 int ret = poll( &fds, 2, 10000 ); // 有事件返回 if ( ret == -1 ) // 出错了 else if ( ret == 0 ) // 超时 else { // 对每个pollfd检测是否有就绪的事件 if ( pfd[0].revents & POLLIN ) pfd[0].revents = 0; // 可读 if ( pfd[1].revents & POLLOUT ) pfd[1].revents = 0; // 可写 }
与select相同,都是创建结构,设置,开始polling,逐个检测事件
相比于SELECT的改进
对于可以监控fd的数量没有限制,而不是像select那样最大才1024个
每次poll之后不需要重新设置pollfd,而不像fd_set需要重新设置
兼容性
vista之前的windows上没有poll
#if defined (WIN32) static inline int poll( struct pollfd *pfd, int nfds, int timeout) { return WSAPoll ( pfd, nfds, timeout ); } #endif
EPOLL
linux平台上最新的polling技术,出现与linux2.6版本,linux2.6发布是在2003年(居然epoll出现已经12年了)。
接口
int epoll_create(int size);
用于创建一个size大小的epoll,返回一个epfd的描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
修改某个文件描述符的状态
epfd 是创建的epoll
op 是修改的操作类型,可以是EPOLL_CTL_ADD 或者 EPOLL_CTL_DEL,代表添加和删除
fd 是要操作的文件描述符
event 是文件描述符fd上挂的一个context,是一个epoll_event结构体,下面是epoll_event的结构体内容:
typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t; struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
其中epoll_event.events 和 pollfd中的events 差不多,不过事件更加丰富,data是对于文件描述符上可以挂的卫星数据,也更加灵活。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epdf 是epoll_create时候返回的
events 是polling 返回的结果会赋值在events
maxevents 是一次通知用户最大的events数量,一般就是events的数组长度
timeout 是超时时间
相比select/poll,epoll_wait 只返回可读写的事件,而不是全部返回
关于几个size的理解
epoll_create 时候的size是指 epoll在核心态监控fd的最大数量
epoll_wait 时候的events 是指一次通知的数据,这个数量认为是一次批量,肯定是小于等于epoll_create时候的大小,比这个再大也没用了
epoll_wait 时候的maxevents 是为了防止一次通知events溢出的一个边界,如果设置的比events的数组长度小,那就相当于批量变小,比这个大会溢出,所以应该是相等就可以了
使用过程
// 创建 int pollingfd = epoll_create( 0xCAFE ); // 创建一个epoll_event 用来一会儿epoll_ctl的时候EPOLL_CTL_ADD用 struct epoll_event ev = { 0 }; // 假设sock1是个网络连接 int sock1 = pConnection1->getSocket(); // 给这个sock1挂一点卫星数据,这里可以是任意的东西,我们就放个他的connection ev.data.ptr = pConnection1; // 来监控sock1的可读事件 ev.events = EPOLLIN | EPOLLONESHOT; // 把设置好的epoll_event 添加到创建的epollfd if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, sock1, &ev ) != 0 ) // 出错 // 创建一些epoll_event用来在用户态来接收 struct epoll_event pevents[ 20 ]; // 等待可读事件 int ready = epoll_wait( pollingfd, pevents, 20, 10000 ); if ( ret == -1 ) // 出错 else if ( ret == 0 ) // 超时 else { // ret是返回了多少个可以读写的事件 for ( int i = 0; i < ret; i++ ) { // 判断通知到用户这个到底是个什么事件 if ( pevents[i].events & EPOLLIN ) { // 取到当初我们挂在上面的卫星数据 Connection * c = (Connection*) pevents[i].data.ptr; // 对这个socket进行一些操作 c->handleReadEvent(); } } }
epoll的使用过程还是比select和poll复杂不少的,首先你得创建一个epoll,然后创建和设置epoll_event,再通过epoll_ctl添加到epoll,最后epoll_wait,遍历通知过来的events
比select和poll的改进
最大的改进就是不需要在遍历所有事件了,不需要FD_ISSET,也不需要遍历所有pollfd.revents,取而代之的是,内核帮我们把active的fd赋值到epoll_wait的events上
pollfd封装了一个event,而不是像select的fd_set只有一个fd属性,epoll_event 比pollfd又多了一个data的卫星数据,可以放任意的东西上去
select和poll一旦进入polling阶段,就没法对fd做修改了,但是epoll_ctl可以在任意的线程里在任意事件动态的添加,删除epoll_event
缺点
改变epoll中fd的监听事件类型需要epoll_ctl的系统调用,而在poll中只需要在用户态做BITMASK
只能在linux上用,虽然有libevent这种东西
epoll的api比select和poll复杂
该如何选择
如果连接数很低小于1024,epoll对比select和poll是没有性能提升的,选择select还是poll就看个人喜好了,一般select就行,比如fpm,epoll早就出了fpm也没改,PHP很少有人能worker开1000以上
如果连接是短连接,经常accept出一些fd添加到epoll中的系统调用开销需要考虑,具体性能还需要再综合考虑,比如nginx,虽然都是短连接,但是有高并发,几万并发select每次遍历一遍所有fd更耗
如果是长连接,并且都是idle的,例如一些聊天的服务器,一个连接,半天才说一句话,都是挂机的,但是连接几十万,那有个人说句话,服务区需要读,你遍历几十万个fd就不值了
如果你的应用是多线程来处理网络的,那么为了利用多线程还是使用epoll比较好,可以用多线程配合边缘触发(如果可读只通知一次,不管读完没读完,水平触发没读完就一直通知,所以效率会比边缘触发低一些),这也是边缘触发推荐的使用方式。
为什么epoll高效
简单来说是这样的select和poll当检测到fd就绪以后,就通知到用户态了,函数也就返回了。而epoll在add的时候就开始监听,发现他就绪以后就放到一个就绪表里,epoll_wait只是定时查看一下这个就绪表里的数据。
参考文章
https://cs.uwaterloo.ca/~brecht/papers/getpaper.php?file=ols-2004.pdf
http://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/
http://www.unix.org/what_is_unix/history_timeline.html
http://en.wikipedia.org/wiki/Asynchronous_I/O