select,poll,epoll简介:
select |
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是: 1、 单个进程可监视的fd数量被限制,数组有大小限制; 2 、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大; 3 、对socket进行扫描时是线性扫描; 4、select也是“水平触发”,如果报告了fd后,没有被处理,那么下次selectl时会再次报告该fd; |
poll |
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。 1、它没有最大连接数的限制,原因是它是基于链表来存储的; 2、和select一样大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。 3、poll也是线性扫描; 4、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。 |
epoll | epoll支持水平触发和边缘触发; 1、最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。 2、在前面说到的复制问题上,epoll使用mmap减少复制开销(使用mmap,内核和用户态共享数据)。 3、还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。 4、虽然也有限制,可以认为无限大。 |
select,poll,epoll的比较:
1、支持一个进程所能打开的最大连接数
select | 单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上 FD_SETSIZE为32*64),当然我们可以对它进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。 |
poll | poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。 |
epoll | 虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。 |
2、FD剧增后带来的IO效率问题
select |
因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。 |
poll | 同上 |
epoll |
因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。 |
3、消息传递方式
select | 内核需要将消息传递到用户空间,都需要内核拷贝动作。 |
poll | 同上 |
epoll | epoll通过内核和用户空间通过mmap共享一块内存来实现的。 |
综上比较:
在选择select,poll,epoll时,要根据具体的使用场合以及select,poll,epoll这三种方式的自身特点。
从表象看epoll的性能最好,但是在连接数少,并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多回调函数来完成。
epoll在事件管理上,使用的是红黑树,可以快速的增删事件和快速查找事件。
以上参考博客:
http://xingyunbaijunwei.blog.163.com/blog/static/76538067201241685556302/
select的接口,及解释:
select()
select()用来监控多个文件描述符,当fd变得可读/可写时,select()将标记可读/可写fd。select()现在被认为是低效的fd监控接口,在实际项目中通常用epoll()来代替select()。
#include <unistd.h> #include <sys/select.h> #include <sys/types.h> #include <sys/time.h> int select(int maxfd, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout); 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);
select()的参数解析:
1、一个参数maxfd是加入select()的最大文件描述符值+1,最大值为1024。可以修改FD_SETSIZE的值以使select()支持更多文件描述符监控,但必须重新编译内核,否则结果未知。
2、中间3个参数是3个fd集合,分别是你想监听的可读fd、可写fd、异常fd。
3、最后一个参数timeout是指定select()的超时时间。
timeout的取值可以是:
NULL - 永久等待,直到有读/写/异常事件发生。
0 - 立即返回,此时select()为非阻塞状态。
其他值 - 指定select()等待时间。注意,timeout指定最长等待时间,但一旦有1个或多个fd可读/写/异常时select()就会返回。
select()的返回值:
0 - 超时,且没有任何读/写fd。
> 0 - 有读/写fd,用FD_ISSET()进一步判断。
-1 - select()出错。常见的错误包括:
EINTR - 捕获到信号。通常可忽略。
EBADF - 有无效的文件描述符。
Socket可读/写的常见情况分析:
select()返回sockfd可读:
1、Receive缓冲区的数据大于或等于low-water mark的值。low-water mark的值可通过SO_RCVLOWAT选项控制,默认是1。 (即读缓冲区中有数据)
2、TCP连接接收到FIN,即Read half of the connections is closed。此时对sockfd的读操作将返回0,即EOF。
3、如果sockfd是一个监听套接字,则表明有新连接,可调用accept()函数建立新连接。
4、Socket出错,此时对sockfd的读操作将返回-1。
select()返回sockfd可写:
1、Send缓冲区的数据大于或等于low-water mark的值。low-water mark的值可通过SO_SNDLOWAT选项控制,默认是2048。 (即缓冲区有数据)
2、Write half of the connection is closed,对sockfd的写操作将产生SIGPIPE信号。
3、对非阻塞的sockfd调用connect(),connect()完成或失败。
4、Socket出错,此时对sockfd的写操作将返回-1。
分析。若读缓冲有数据,则socket可读;若写缓冲有空间,则socket可写。如果socket出错,则它本身处于可读写状态,且调用read()/write()返回-1。若是Listen Socket,则有新连接来时它可读;若是Non-block Connect Socket,则连接成功时它可写。这些情况都不难理解,只有以上列出情况3,需要进一步说明。
参考:http://www.berlinix.com/dev/network.php
epoll的接口非常简单,一共就三个函数:
1.创建epoll句柄
int epfd = epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
2.将被监听的描述符添加到epoll句柄或从epool句柄中删除或者对监听事件进行修改。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
当产生了一个EPOLLIN事件后:
读数据的时候需要考虑的是当recv()返回的大小如果等于要求的大小,即sizeof(buf),那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取(如果在ET水平模式下,可以等到下次事件触发时,再读数据):
while(rs) //ET模型
{
buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
if(buflen < 0)
{
// 由于是非阻塞的模式,所以当errno为EAGAIN或EINT时,表示当前缓冲区已无数据可读(被其它线程可能读了)
// 在这里就当作是该次事件已处理处。因为是ET模式,如果有数据,内核还会继续触发读事件。
//即当buflen<0且errno=EAGAIN||errno=EINT时,表示没有数据了。(读/写都是这样)
if(errno == EAGAIN || errno == EINT)
break;
else
return; //真的失败了。
}
else if(buflen == 0)
{
// 这里表示对端的socket已正常关闭,收到了FIN包。soket在read到0个数据时,表示收到对端请求的FIN包。
}
if(buflen == sizeof(buf)
rs = 1; // 需要再次读取(有可能是因为数据缓冲区buf太小,所以数据没有读完,可以等到下次再读,没有buf了)
else
rs = 0; //不需要再次读取(当buflen<sizeof(buf)时,非阻塞文件描述符的特性),
}
当产生了一个EPOLLOUT事件后:
还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send()函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据。所以,需要封装socket_send()的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回-1表示出错。在socket_send()内部,当写缓冲已满(send()返回-1,且errno为EAGAIN),那么会等待后再重试。这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send()内部,但暂没有更好的办法。这种方法类似于readn和writen的封装(在《UNIX环境高级编程》中也有介绍)
ssize_t socket_send(int sockfd, const char* buffer, size_t buflen)
{
ssize_t tmp;
size_t total = buflen;
const char *p = buffer;
while(1)
{
tmp = send(sockfd, p, total, 0);
if(tmp < 0)
{
// 当send收到信号时,可以继续写,但这里返回-1.
if(errno == EINTR)
return -1;
// 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满,
// 在这里做延时后再重试.
if(errno == EAGAIN)
{
usleep(1000);
continue;
}
return -1;
}
if((size_t)tmp == total)
return buflen;
total -= tmp;
p += tmp;
}
return tmp;
}