面试的时候常常被问的一个很蛋疼的问题,常常被问,可是知识很零散,难记忆,看完就忘php
能够监视文件描述符是否能够读写,要求监视的文件描述符是非阻塞的html
产生与上个世纪80年代的UNIX系统,到1993年写入POSIX1.b规范(一个操做系统的编程接口的规范,你要是写个操做系统想被兼容得遵照这个规范)。因为那个年代尚未多线程(2年后线程相关的内容才写入POSIX1.c规范),尚未什么C10K问题,因此在设计select的时候体现了那个年代的特色。linux
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds 是参数2,3,4中最大的文件描述符 + 1nginx
readfds 是要检测fd的可读事件,当fd可读的时候select就返回面试
writefds 是要检测fd的写事件,当fd可写的时候select就返回编程
excpetfds 是要检测fd的出错事件,当fd出错的时候select就返回,当是NULL的时候,就是检测readfds和writefds的出错事件,因此通常都写NULLwindows
timeout 是一个纳秒级的超时时间api
想使用这个函数须要设置个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标准因此跨平台比较好
功能和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,逐个检测事件
对于能够监控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
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
最大的改进就是不须要在遍历全部事件了,不须要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比较好,能够用多线程配合边缘触发(若是可读只通知一次,无论读完没读完,水平触发没读完就一直通知,因此效率会比边缘触发低一些),这也是边缘触发推荐的使用方式。
简单来讲是这样的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