版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
1. select
① select函数
- select函数允许监视一组文件描述符,等待一个或多个描述符成为就绪状态,从而完成I/O操作。
- select函数的原型如下:
#include<sys/select.h>
#include<sys/time.h>
int select(int maxfdpl, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- maxfdpl: 指定监视的描述符个数,值为最大描述符加1,即
max fd plus one
。 - fd_set: 通常是一个整数数组,整数中的每一位对应一个描述符。
<sys/select.h>
中的常数FD_SETSIZE
是fd_set中描述符的总数,32位系统中FD_SETSIZE
我1024,64位系统中FD_SETSIZE
为2048。fd_set有三种描述符集:reedset
(读描述符集)、writeset
(写描述符集)、exceptset
(异常描述符集)。如果对某个描述符集不感兴趣,可以将该描述符集设置为null。 - timeout: 超时参数,数据类型为
timeval
,调用select函数会一直阻塞直到有描述符就绪或等待的时间超过timeout。超时参数三种设置:设置为null,表示永久等待,直到有描述符就绪;设置为某个整数,表示等待一段时间;设置为0,表示select函数立即返回,即采用轮询方式。 - 如有描述符就绪,select函数返回描述符就绪的个数;若等待超时,select函数返回0;运行出错,select函数返回-1。
- timeout的数据结构是
timeval
,两个参数如下:
struct timeval{
long tv_sec; // 单位秒
long tv_usec; // 单位微妙
}
② 对描述符集的操作
- 低水位标记:
- 接收缓冲区低水位标记:规定接收缓冲区最少有多少数据时,可以从接收缓冲区中读取数据。
- 发送缓冲区低水位标记:规定发送缓冲区最少有多少剩余空间时,可以向发送缓冲区写数据。
- 可以通过
SO_RCVLOWAT
套接字选项设置接收缓冲区的低水位标记,默认为1。 - 可以通过
SO_SNDLOWAT
套接字选项设置发送缓冲区的低水位标记,默认为2048。
- 对描述符集的操作:
void FD_ZERO(fd_set *fdset); // 清除fdset中的所有位
void FD_SET(int fd,fd_set *fdset); // 打开dfset中fd位
void FD_CLR(int fd,fd_set *fdset); //清除fdset中的fd位
int FD_ISSET(int fd,fd_set *fdset); // 检查fdset中的fd位是否被置位
- select函数的使用举例:
- 通过
FD-ZERO()
函数,将描述符集清零。 - 通过
FD_SET()
函数,将描述符集中的fd位。 - 通过比较多个fd值得大小,确定
maxfdpl
的值。 - 设置timeout参数,然后调用select函数。如果对某个描述符集不感兴趣,将其置为null。
- select函数的返回为0表示超时,-1表示出错;大于0则可以通过
FD_ISSET()
函数检测是否为某个fd就绪。
fd_set fd_in, fd_out;
struct timeval tv;
// Reset the sets
FD_ZERO( &fd_in );
FD_ZERO( &fd_out );
// Monitor sock1 for input events
FD_SET( sock1, &fd_in );
// Monitor sock2 for output events
FD_SET( sock2, &fd_out );
// Find out which socket has the largest numeric value as select requires it
int largest_sock = sock1 > sock2 ? sock1 : sock2;
// Wait up to 10 seconds
tv.tv_sec = 10;
tv.tv_usec = 0;
// Call the select
int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv );
// Check if select actually succeed
if ( ret == -1 )
// report error and abort
else if ( ret == 0 )
// timeout; no event detected
else
{
if ( FD_ISSET( sock1, &fd_in ) )
// input event on sock1
if ( FD_ISSET( sock2, &fd_out ) )
// output event on sock2
}
③ select函数的优缺点
- 优点: 可移植性好,几乎所有的平台都支持select函数。
- 缺点:
- 能打开的fd数量有限: 操作符集中fd总数由内核中
FD_SETSIZE
常量决定,32位系统为1024,64位系统为2048。对于有大量连接的服务器来说,fd的数量根本不能满足需求。如果要增大FD_SETSIZE
的值,需要重新编译内核。 - 查询就绪fd的开销大: select函数返回大于0时,需要遍历指定的fd,查询哪些fd已就绪。
- 用户态与内核态之间复制描述符的开销比较大。
2. poll
① poll函数
- poll函数与select函数的功能一样,监视一组描述符,等待其中一个或多个描述符成为就绪状态,从而完成I/O操作。
- poll函数的原型如下:
int poll(struct pollfd *fdarray, unsigned int nfds, int timeout);
- fdarray: 指向
pollfd
数组的指针,pollfd
中存储了描述符。 - nfds:
pollfd
数组的大小,即要监听的描述符个数。 - timeout: 超时参数,数据类型为
int
,poll函数一直阻塞直到有描述符就绪或等待的时间超时。timeout
参数为负值,表示poll函数一直阻塞直到有描述符就绪;timeout
大于0,表示等待一段时间;timeout
为0,表示立即返回,需要通过轮询监视描述符状态。 - 如果有描述符就绪,poll函数返回就绪的描述符个数;如果等待超时,poll函数返回0;如果发生错误,poll函数返回-1。
- poll函数的使用举例:
// The structure for two events
struct pollfd fds[2];
// Monitor sock1 for input
fds[0].fd = sock1;
fds[0].events = POLLIN;
// Monitor sock2 for output
fds[1].fd = sock2;
fds[1].events = POLLOUT;
// Wait 10 seconds
int ret = poll( &fds, 2, 10000 );
// Check if poll actually succeed
if ( ret == -1 )
// report error and abort
else if ( ret == 0 )
// timeout; no event detected
else
{
// If we detect the event, zero it out so we can reuse the structure
if ( fds[0].revents & POLLIN )
fds[0].revents = 0;
// input event on sock1
if ( fds[1].revents & POLLOUT )
fds[1].revents = 0;
// output event on sock2
}
② pollfd数据结构
- 关于
pollfd
结构:
struct pollfd{
int fd; // 监听的描述符
short events; // 该描述符要监听的事件
short revents; // 该描述符上发生的事件
}
- 在poll函数中,不再使用fd_set存储描述符,而是将描述符封装成pollfd结构,使用pollfd数组存储描述符。
- pollfd包含描述符、该描述符监听的事件、该描述符上发生的事件。
- 针对读描述符、写描述符、异常描述符,pollfd定义了不同的事件分别对应不同种类的描述符。
- 如果不想再关心
fdarray
中的某个描述符,可以将其pollfd中的fd设置为负数,poll函数将忽略其events
成员,返回时将revnets
成员的值设置为0。
③ poll函数与select函数的比较
- 相同点:
- 功能: select和poll函数都可以对一组描述符进行监听,等待其中的一个或某些描述符就绪,从而完成I/O操作。
- 轮询: select函数和poll函数都需要通过轮询获取已就绪的描述符。
- 开销大: select函数poll函数的速度都比较慢,因为需要在用户态和内核态之间复制描述符,导致开销比较大。
- 非线程安全: 如果一个线程对某个描述符调用了select或者poll函数,另一个线程关闭了该描述符,会导致调用结果不确定。
- 不同点:
- 描述符数量: select函数的描述符数量受内核中
FD_SETSIZE
常量的限制,而poll函数没有描述符数量的限制。 - 可移植性: select函数被几乎所有的平台支持,而poll函数只有比较新的平台支持。
- 修改描述符: select函数会修改描述符,poll函数不会。
- 利用率: poll函数提供了更多的事件类型,对描述符的重复利用比select函数高。
3. epoll
① epoll函数族
- epoll不在是一个函数,而是一个函数族。并且,epoll句柄本身将占用一个描述符。
- epoll函数族如下:
#include <sys/epoll.h>
int epoll_create(int size);
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);
epoll_create
函数:
- 创建一个epoll句柄,会占用一个描述符,简写为
epfd
。 - 使用完epoll后,必须通过close() 进行关闭。
- 参数size: 告诉内核需要监听的描述符数量,但不是最大数量,而是初始分配数据结构时的一个建议。
epoll_ctl
函数:对描述符fd执行操作op。
- EPOLL_CTL_ADD: 注册新的fd到epfd中,即向内核注册新的描述符。
- EPOLL_CTL_DELETE: 从epfd中删除一个fd,即从内核中删除一个描述符。
- EPOLL_CTL_MOD: 修改已注册fd的监听事件为event。
- 监听事件event中,有个
EPOLLET
的宏,设置epoll为边缘触发,默认是水平触发(LT)。
epoll_wait
函数:等待epoll句柄上的I/O事件,最多返回maxevents
个事件
② 工作模式
- epoll的描述符事件有两种触发方式:水平触发(LT)和边缘触发(ET)。
- 水平触发(LT):
epoll_wait()
函数检测到描述符事件发生时,会将此事件通知应用进程。- 应用进程可以不必立即进行处理,下次调用
epoll_wait()
函数会再次通知应用进程。 - 默认为水平触发,支持阻塞和非阻塞模式。
- 边缘触发(ET):
epoll_wait()
函数检测到描述符事件的发生,会将此事件通知应用进程。- 应用进程必须立即进行处理,如果不处理,下次调用
epoll_wait()
函数不会再次通知应用进程。 - 边缘触发只支持非阻塞模式,以避免对一个描述符的阻塞读/写操作将处理多个描述符的任务饿死。
- ET模式在很大程度上减少了epoll事件被重复触发的次数,效率比LT模式高。
③ epoll与select/poll的比较
- 从描述符数量、是否需要轮询、可移植性、线程安全、开销、触发方式、速度对三者进行比较:
- 描述符数量: epoll比select和poll更加灵活,而且没有描述符数量的限制。
- 轮询: select和poll需要对描述符进行轮询,以获取就绪的描述符;而epoll直接获得只有就绪描述符的链表,无需轮询。
- 开销: select和poll每次调用需要将全部描述符从应用进程缓冲区拷贝到内核缓冲区,而epoll只需要拷贝一次。
- 可移植性: 几乎所有的平台都支持select,较新的平台支持poll,而只有Linux操作系统支持epoll。
- 线程安全: 在一个线程中某个描述符调用select或poll方法,在另一个线程中关闭该描述符,到导致调用的结果将不确定;而epoll对多线程编程的支持较好,在一个线程中调用了
epoll_wait()
方法,在另一个线程中关闭同一个描述符,也不会出现结果不确定的情况。 - 触发方式: select和poll都只支持水平触发,而epoll还支持更高效的边缘触发。
- 速度: 只有在连接数较多且活跃连接较少的情况,才能体现epoll的高效;如果连接数较少且都十分活跃,select和epoll的速度差距并不大,甚至select和poll的性能可能会更好。
④ 三者的选择
- select的应用场景:
- select的timeout参数精度为
us
,而poll和epoll为ms
。select更加适合对实时性要求高的场景,比如核反应堆的控制。 - select的可移植性更好,几乎所有的平台都支持select。
- poll的应用场景:
- poll没有描述符限制,如果对实时性要求不高,应该选择poll而不是select。
- epoll的应用场景:
- epoll只能运行在Linux平台上,而且要求连接数较多,最好是长连接的情况(最好超过1000个)。