I/O多路复用(select、poll、epoll)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/u014454538/article/details/100091497

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);
  1. maxfdpl: 指定监视的描述符个数,值为最大描述符加1,即max fd plus one
  2. fd_set: 通常是一个整数数组,整数中的每一位对应一个描述符<sys/select.h>中的常数FD_SETSIZE是fd_set中描述符的总数,32位系统中FD_SETSIZE1024,64位系统中FD_SETSIZE2048。fd_set有三种描述符集:reedset(读描述符集)、writeset(写描述符集)、exceptset(异常描述符集)。如果对某个描述符集不感兴趣,可以将该描述符集设置为null。
  3. timeout: 超时参数,数据类型为timeval,调用select函数会一直阻塞直到有描述符就绪或等待的时间超过timeout。超时参数三种设置:设置为null,表示永久等待,直到有描述符就绪;设置为某个整数,表示等待一段时间;设置为0,表示select函数立即返回,即采用轮询方式。
  4. 如有描述符就绪,select函数返回描述符就绪的个数;若等待超时,select函数返回0;运行出错,select函数返回-1
  • timeout的数据结构是timeval,两个参数如下:
struct timeval{
	long tv_sec; // 单位秒
	long tv_usec; // 单位微妙
}
② 对描述符集的操作
  • 低水位标记:
  1. 接收缓冲区低水位标记:规定接收缓冲区最少有多少数据时,可以从接收缓冲区中读取数据。
  2. 发送缓冲区低水位标记:规定发送缓冲区最少有多少剩余空间时,可以向发送缓冲区写数据。
  3. 可以通过SO_RCVLOWAT套接字选项设置接收缓冲区的低水位标记,默认为1。
  4. 可以通过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函数的使用举例:
  1. 通过FD-ZERO()函数,将描述符集清零。
  2. 通过FD_SET()函数,将描述符集中的fd位。
  3. 通过比较多个fd值得大小,确定maxfdpl的值。
  4. 设置timeout参数,然后调用select函数。如果对某个描述符集不感兴趣,将其置为null。
  5. 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函数。
  • 缺点:
  1. 能打开的fd数量有限: 操作符集中fd总数由内核中FD_SETSIZE常量决定,32位系统为1024,64位系统为2048。对于有大量连接的服务器来说,fd的数量根本不能满足需求。如果要增大FD_SETSIZE的值,需要重新编译内核。
  2. 查询就绪fd的开销大: select函数返回大于0时,需要遍历指定的fd,查询哪些fd已就绪。
  3. 用户态与内核态之间复制描述符的开销比较大。

2. poll

① poll函数
  • poll函数与select函数的功能一样,监视一组描述符,等待其中一个或多个描述符成为就绪状态,从而完成I/O操作。
  • poll函数的原型如下:
int poll(struct pollfd *fdarray, unsigned int nfds, int timeout);
  1. fdarray: 指向pollfd数组的指针,pollfd中存储了描述符。
  2. nfds: pollfd数组的大小,即要监听的描述符个数。
  3. timeout: 超时参数,数据类型为int,poll函数一直阻塞直到有描述符就绪或等待的时间超时。timeout参数为负值,表示poll函数一直阻塞直到有描述符就绪;timeout大于0,表示等待一段时间;timeout0,表示立即返回,需要通过轮询监视描述符状态。
  4. 如果有描述符就绪,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; // 该描述符上发生的事件
}
  1. 在poll函数中,不再使用fd_set存储描述符,而是将描述符封装成pollfd结构,使用pollfd数组存储描述符
  2. pollfd包含描述符、该描述符监听的事件、该描述符上发生的事件。
  3. 针对读描述符、写描述符、异常描述符,pollfd定义了不同的事件分别对应不同种类的描述符
  4. 如果不想再关心fdarray中的某个描述符,可以将其pollfd中的fd设置为负数,poll函数将忽略其events成员,返回时将revnets成员的值设置为0。
③ poll函数与select函数的比较
  • 相同点:
  1. 功能: select和poll函数都可以对一组描述符进行监听,等待其中的一个或某些描述符就绪,从而完成I/O操作。
  2. 轮询: select函数和poll函数都需要通过轮询获取已就绪的描述符。
  3. 开销大: select函数poll函数的速度都比较慢,因为需要在用户态和内核态之间复制描述符,导致开销比较大。
  4. 非线程安全: 如果一个线程对某个描述符调用了select或者poll函数,另一个线程关闭了该描述符,会导致调用结果不确定
  • 不同点:
  1. 描述符数量: select函数的描述符数量受内核中FD_SETSIZE常量的限制,而poll函数没有描述符数量的限制。
  2. 可移植性: select函数被几乎所有的平台支持,而poll函数只有比较新的平台支持。
  3. 修改描述符: select函数会修改描述符,poll函数不会。
  4. 利用率: 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函数:
  1. 创建一个epoll句柄,会占用一个描述符,简写为epfd
  2. 使用完epoll后,必须通过close() 进行关闭。
  3. 参数size: 告诉内核需要监听的描述符数量,但不是最大数量,而是初始分配数据结构时的一个建议。
  • epoll_ctl函数:对描述符fd执行操作op。
  1. EPOLL_CTL_ADD: 注册新的fd到epfd中,即向内核注册新的描述符。
  2. EPOLL_CTL_DELETE: 从epfd中删除一个fd,即从内核中删除一个描述符。
  3. EPOLL_CTL_MOD: 修改已注册fd的监听事件为event。
  4. 监听事件event中,有个EPOLLET的宏,设置epoll为边缘触发默认是水平触发(LT)。
  • epoll_wait函数:等待epoll句柄上的I/O事件,最多返回maxevents个事件
② 工作模式
  • epoll的描述符事件有两种触发方式:水平触发(LT)和边缘触发(ET)。
  • 水平触发(LT):
  1. epoll_wait()函数检测到描述符事件发生时,会将此事件通知应用进程。
  2. 应用进程可以不必立即进行处理,下次调用epoll_wait()函数会再次通知应用进程。
  3. 默认为水平触发,支持阻塞非阻塞模式。
  • 边缘触发(ET):
  1. epoll_wait()函数检测到描述符事件的发生,会将此事件通知应用进程。
  2. 应用进程必须立即进行处理,如果不处理,下次调用epoll_wait()函数不会再次通知应用进程。
  3. 边缘触发只支持非阻塞模式,以避免对一个描述符的阻塞读/写操作将处理多个描述符的任务饿死。
  4. ET模式在很大程度上减少了epoll事件被重复触发的次数,效率比LT模式高。
③ epoll与select/poll的比较
  • 从描述符数量、是否需要轮询、可移植性、线程安全、开销、触发方式、速度对三者进行比较:
  1. 描述符数量: epoll比select和poll更加灵活,而且没有描述符数量的限制。
  2. 轮询: select和poll需要对描述符进行轮询,以获取就绪的描述符;而epoll直接获得只有就绪描述符的链表,无需轮询。
  3. 开销: select和poll每次调用需要将全部描述符从应用进程缓冲区拷贝到内核缓冲区,而epoll只需要拷贝一次。
  4. 可移植性: 几乎所有的平台都支持select,较新的平台支持poll,而只有Linux操作系统支持epoll。
  5. 线程安全: 在一个线程中某个描述符调用select或poll方法,在另一个线程中关闭该描述符,到导致调用的结果将不确定;而epoll对多线程编程的支持较好,在一个线程中调用了epoll_wait()方法,在另一个线程中关闭同一个描述符,也不会出现结果不确定的情况。
  6. 触发方式: select和poll都只支持水平触发,而epoll还支持更高效的边缘触发。
  7. 速度: 只有在连接数较多且活跃连接较少的情况,才能体现epoll的高效;如果连接数较少且都十分活跃,select和epoll的速度差距并不大,甚至select和poll的性能可能会更好。
④ 三者的选择
  • select的应用场景:
  1. select的timeout参数精度为us,而poll和epoll为ms。select更加适合对实时性要求高的场景,比如核反应堆的控制。
  2. select的可移植性更好,几乎所有的平台都支持select。
  • poll的应用场景:
  1. poll没有描述符限制,如果对实时性要求不高,应该选择poll而不是select。
  • epoll的应用场景:
  1. epoll只能运行在Linux平台上,而且要求连接数较多,最好是长连接的情况(最好超过1000个)。

猜你喜欢

转载自blog.csdn.net/u014454538/article/details/100091497