1 I/O多路复用
1.1 主旨思想
在之前使用多进程或多线程实现服务端与客户端的通信时,对于每一个连接成功的文件描述符都需要一个进程或线程来进行监控,因此需要消耗大量的系统资源。如果是使用非阻塞的I/O模型,则又需要采用轮询的方式,获取文件描述符状态,虽然一定程度上能够提高程序的执行效率,但却需要占用更多的CPU和系统资源。
因此,解决这一问题比较好的方式就是采用I/O多路复用模型,将阻塞过程提前到select/poll/epoll中,不必建立新的进程或线程,直接让内核去等待,只需要主进程去直接询问内核哪些描述符准备好即可,能够极大地减少系统开销。其实现步骤大致可表述为:
- 首先构造文件描述符列表,将要监听的文件描述符添加到该列表中;
- 调用一个阻塞系统函数,监听该列表中的文件描述符,直到这些文件描述符中的一个或者多个进行I/O操作时,该函数才会返回。
- 函数返回后,进程会被告知有哪些描述符已准备好可以进行IO操作。
select(),poll(),epoll()都是I/O多路复用的机制。I/O多路复用通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪,就是这个文件描述符进行读写操作之前),能够通知程序进行相应的读写操作。但select(),poll(),epoll()本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
2 select函数
2.1 函数原型
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- nfds:要监视的文件描述符的范围,一般取监视的最大文件描述符编号值+1;
- readfs、writefds、exceptfds:指向描述符集的指针,说明了可读、可写或处于异常条件的描述符集合,每个描述符集存储在一个fd_set数据类型中。对于fd_set数据类型,唯一可以进行的处理是:分配一个这种类型的变量,将这种类型的一个变量值赋给同类型的另一个变量,或通过以下四个宏进行设置:
void FD_ZERO(fd_set *fdset); //清空集合
void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合
void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除
int FD_ISSET(int fd, fd_set *fdset);//检查集合中指定的文件描述符是否可以读写
- timeout:超时时间,它告知内核所指定的等待描述符就绪的时间:
struct timeval
{
long tv_sec;
long tv_usec;
};
//NULL : 永久等待,直到检测到有文件描述符就绪
//tv_sec = 0 tv_usec = 0, 不阻塞,检查描述符后立即返回,即轮询
//tv_sec > 0 tv_usec > 0, 等待固定时间
- 返回值:超时返回0;失败返回-1;成功返回大于0的整数,即就绪描述符的数目。
2.2 优缺点
优点:
- 移植性好,可跨平台运行;
- 超时时间设置精度高,可精确到微秒,而poll为毫秒。
缺点:
- 单个进程可监视的文件描述符数目上限为1024,若更改需修改内核相关代码;但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,效率也就越低;
- select每次调用时都要将文件描述符集合从用户态拷贝到内核态,开销较大;
- select每次调用都需要在内核遍历传递进来的文件描述符集合,当文件描述符数量很大时也会产生较大的开销;
- select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
3 程序示例
3.1 服务端程序
#include <iostream>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define SERVER_PORT 9998
#define MAX_CON 8
using namespace std;
int main() {
sockaddr_in lskt, cskt;
sockaddr_in Clientaddr[