I/O复用基本概念
I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降底了系统的维护工作量,节省了系统资源。
select模型
使用select函数时可以将多个文件描述符集中到一起统一监视。监视项成为“事件”(event)。
-
select函数调用方法和顺序:
步骤一:设置文件描述符、指定监视范围、设置超时
步骤二:调用select函数
步骤三:查看调用结果
-
1、设置文件描述符
利用select函数可以同时监视多个文件描述符,首先需要将要监视的文件描述符集中到一起。集中时要按照监视项(接收、传输、异常)进行区分,即按照上述3中监视项分成3类。
使用fd_set数组变量执行此项操作。如果该位设置为1,则表示该文件描述符是监视对象。
在fd_set变量中注册或更改值的操作都由下列宏完成:
FD_ZERO(fd_set *fdset):将fd_set变量的所有位初始化为0。
FD_SET(int fd,fd_set *fdset):在参数fdset指向的变量中注册文件描述符fd的信息。
FD_CLR(int fd,fd_set *fdset):从参数fdset指向的变量中清楚文件描述符fd的信息。
FD_ISSET(int fd,fd_set * fdsest):若参数fdset指向的变量中包含文件描述符fd的信息,则返回“真”。
-
2、设置检查(监视)范围及超时
int select(
int maxfd,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval * timeout
);
成功时返回大于0的值,失败时返回-1。发生错误时返回-1,超时返回时返回0。因发生关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符。
参数解释:
1、maxfd:监视对象文件描述符
2、readset:将所有关注“是否存在待读取数据”的文件描述符注册到fd_set型变量,并传递其地址值
3、writeset:将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set型变量,并传递其地址值
4、exceptset:将所有关注“是否发生异常” 的文件描述符注册到fd_set型变量,并传递其地址值
5、timeout:调用select函数后,为防止陷入无限阻塞的状态,传入超时(time-out)信息
- select函数用来验证3种监视项的变化情况。根据监视项声明3个fd_set变量,分别向其注册文件描述符信息,并把变量的地址值传递到上述函数的第二个到第四个参数。
- select函数只有在监视的文件描述符发生变化时才返回。如果未发生变化,就会进入阻塞状态。将秒数填入tv_sec成员,将微秒数填入tv_user成员,然后将结构体的地址值传递到select函数的最后一个参数。
timeval结构体定义:
struct timeval
{
long tv_sec;//seconds
long tv_usec;//microseconds
}
-
3、调用select函数后查看结果
如果返回大于0的整数,说明相应数量的文件描述符发生变化。
文件描述符的变化是指监视的文件描述符中发生了相应的监视事件。
select函数调用完成后,向其传递的fd_set变量中将发生变化。原来为1的所有位均变为0,但发生变化的文件描述符对应位除外。因此,可以认为值仍未1的位置上的文件描述符发生了变化。
-
4、基于Windows的实现
int select(
int nfds,fd_set *readfds,fd_set *writefds,fd_set *excepfds,const struct timeval *timeout);
成功时返回0,失败时返回-1
timeval结构体定义
typedef struct timeval
{
long tv_sec;//seconds
long tv_usec;//microseconds
}
【重点】Windows中的fd_set结构体
typedef struct fd_set
{
u_int fd_count;
SOCKET fd_array[FD_SETSIZE];
}fd_set;
Windows的fd_set由成员fd_count和fd_array构成,fd_count用于套接字句柄数,fd_array用于保存套接字句柄。
原因:Linux的文件描述符从0开始递增,因此可以找出当前文件描述符数量和最后生成的文件描述符之间的关系。但Window的套接字句柄并非从0开始,而且句柄的整数值之间并无规律可循,
因此需要直接保存句柄的数组和记录句柄数的变量。
PS:处理fd_set结构体的FD_XXX型的4个宏的名称、功能及使用方法与Linux完全相同。
优于select的epoll【Linux】
1、与传统select模型的对比
- 基于select的I/O复用缺点
1、调用select函数后常见的针对所有文件描述符的循环语句。
2、每次调用select函数时都需要向该函数传递监视对象信息。
应用程序向操作系统传递数据将对程序造成很大负担。有些函数不需要操作系统的帮助就能完成功能,有些则必须借助于操作系统。select函数与文件描述符有关,是监视套接字变化的函数。而套接字是由操作系统管理的,所以select函数绝对需要借助于操作系统才能完成功能。 - 解决方法:
仅向操作系统传递1次监视对象,监视范围或内容发生变化时只通知发生变化的事项。Linux的支持方式是epoll,Windows的支持方式是IOCP。 - select优点
大部分操作系统都支持select函数,因此基于select的I/Of复用具有兼容性。
服务器接入者少并且程序应具有兼容性时,应考虑使用select模型。
2、实现epoll时必要的函数和结构体
- epoll函数的优点
1、无需编写以监视状态变化为目的的针对所有文件描述符的循环语句。
2、调用对应于select函数的epoll_wait函数时无需每次传递监视对象信息。 - epoll服务器端实现中需要的三个函数
1 、epoll_create : 创建保存epoll文件描述符的空间。
2、epoll_clt:向空间注册并注销文件描述符。
3、epoll_wait:与select函数类似,等待文件描述符发生变化。
- epoll方式下由操作系统负责保存监视对象文件描述符,因此需要向操作系统请求创建保存文件描述符的空间,调用epoll_create函数。
- 在epoll方式中,通过epoll_clt函数请求操作系统完成添加和删除监事对象文件描述符。
- epoll中调用epoll_wait函数等待文件描述符的变化。
- epoll方式中通过如下结构体epoll_event将发生变化的(发生事件的)文件描述符单独集中到一起。
struct epoll_event
{
__unit32_t events;
epoll_data_t data;
}
typedef union epoll_data
{
void *ptr;
int fd;
__unit32_t u32;
__unit64_t u64;
}epoll_data_t;
声明足够大的epoll_event结构体数组后,传递给epoll_wait函数时,发生变化的文件描述符信息将被填入该数组。因此,无需像select函数那样针对所有文件描述符进行循环。
epoll_create
int epoll_create(int size);
成功时返回epoll文件描述符,失败时返回-1.
参数解释:
size: epoll实例大小。
该函数返回的文件描述符主要用于区分epoll例程。需要终止时,与其他文件描述符相同,也要调用close函数。
epoll_ctl
- 生成epoll例程后,应在其内部注册监视对象文件描述符,此时使用epoll_clt.
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
成功时返回0,失败时返回-1。
参数解释:
1、epfd:用于注册监视对象的epoll例程的文件描述符。
2、op:用于指定监视对象的添加、删除或更改等操作。
3、fd:需要注册的监视对象文件描述符。
4、event:监视对象的事件类型。
- 向第二个参数传递的常量及含义:
1、EPOLL_CTL_ADD:将文件描述符注册到epoll例程
2、EPOLL_CTL_DEL:从epoll例程中删除文件描述符
3、EPOLL_CTL_MOD:更改注册的文件描述符的关注事件发生情况
PS:向epoll_ctl的第二个参数传递EPOLL_CTL_DEL时,应同时向第四个参数传递NULL。
- epoll_event结构体用于保存发生事件的文件描述符集合。也可以在epoll例程中注册文件描述符时,用于注册关注的事件。
struct epoll_event event;
...
event.events = EPOLLIN;//发生需要读取数据的情况(事件)时
event.data.fd = sockfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event);
- epoll_event的成员events中可以保存的常量及所指的事件类型:
EPOLLIN:需要读取数据的情况
EPOLLOUT:输出缓冲为空,可以立即发送数据的情况
EPOLLPRI:收到OOB数据的情况
EPOLLRDHUP:断开连接或半关闭的情况,这在边缘触发方式下非常有用
EPOLLERR:发生错误的情况
EPOLLET:以边缘触发的方式得到事件通知
EPOLLONESHOT:发生一次事件后,相应文件描述符不再收到事件通知。因此需要向epoll_ctl函数的第二个参数传递EPOLL_CTL_MOD,再次设置事件。
可以通过位或运算同时传递多个上述参数。
epoll_wait
- epoll_wait与select函数对应,epoll相关函数中默认最后调用该函数。
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
成功时返回发生事件的文件描述符,失败时返回-1。
参数解释:
1、epfd:表示事件发生监视范围的epoll例程的文件描述符。
2、events:保存发生事件的文件描述符集合的结构体地址值。
3、maxevents:第二个参数中可以保存的最大事件数。
4、timeout:以1/1000秒为单位的等待时间,传递-1时,一直等待直到发生事件。
- 第二个参数所指缓冲需要动态分配。
int event_cnt;
struct epoll_event *ep_events;
...
ep_events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE);//EPOLL_SIZE是宏常量
...
event_cnt = epoll_wait(epfd,ep_events,EPOLL_SIZE,-1);
- 函数调用后,返回发生事件的文件描述符数,同时在第二个参数指向的缓冲中保存发生事件的文件描述符集合。因此,无需像select那样插入针对所有文件描述符的循环。
条件触发和边缘触发
- 条件触发方式中,只要输入缓冲有数据就会一直通知该事件。
- 边缘触发中输入缓冲收到数据时仅注册1次该事件。即使输入缓冲中还留有数据,也不会再进行注册。
IOCP 完成端口【Windows】
异步通知I/O模型
- 异步通知I/O模型
异步I/O是指I/O函数的返回时刻与数据收发的完成时刻不一致,异步方式能够比同步方式更有效地使用CPU。
异步通知I/O中指定I/O监视对象的函数和实际验证状态变化的函数是相互分离的。因此,指定监视对象后可以离开执行其他任务,最后再回来验证状态变化。
设置超时时间可以在未发生I/O状态变化的状态下防止函数阻塞,所以可以编写类似异步方式的代码。
- WSAEventSelect函数和通知I/O状态的变化分为:
1、套接字的变化:套接字的I/O状态变化
2、发生套接字相关事件:发生套接字I/O相关事件 - WSAEventSelect 用于指定某一套接字为事件监视对象
int WSAEventSelect(SOCKET s,WSAEVENT hEventObject,long lNetworkEvents);
成功时返回0,失败时返回SOCKET_ERROR
参数解释:
1、s:监视对象的套接字句柄
2、hEventObject:传递事件对象句柄以验证事件发生与否
3、lNetworkEvents:希望监视的事件类型信息
- 传入参数s的套接字内只要发生lNetworkEvents中指定的事件之一,WSAEventSelect函数就将hEventObject句柄所指内核对象改为signal状态。因此,该函数又称“连接事件对象和套接字的函数”。
无论事件发生与否,WSAEventSelect函数调用后都会直接返回,所以可以执行其他任务。 - 第三个参数的事件类型信息:(可以通过位或运算同时指定多个信息)。
FD_READ:是否存在需要接收的数据?
FD_WRITE:是否能以非阻塞方式传输数据?
FD_OOB:是否收到带外数据?
FD_ACCEPT:是否有新的连接请求?
FD_CLOSE:是否有断开连接的请求?
WSAEventSelect函数只能针对1个套接字对象使用。