IO多路复用:select、poll、epoll

一、同步异步、阻塞非阻塞的概念区分

首先,一个 输入操作通常包括两个不同的阶段:

(1)等待数据准备好

(2)从内核向进程复制数据

对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从缓冲区复制到应用进程缓冲区。

1.同步与异步
同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
换句话说,就是由调用者主动等待这个调用的结果。

而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

2.阻塞与非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

举个例子来帮助理解同步异步、阻塞非阻塞:

假如你用水壶烧水。

(1)你把普通水壶放在电磁炉上,站在一旁等水开。(同步阻塞)

(2)你把普通水壶放在电磁炉上,然后去客厅看电视了,时不时去厨房看看水开了没有。(同步非阻塞)

(3)你把煮开时会发出提示声的改进水壶放在电磁炉上,站在一旁等水开。(异步阻塞)

(4)你把煮开时会发出提示声的改进水壶放在电磁炉上,然后去客厅看电视了,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)

对于访问数据,阻塞非阻塞是说针对在得知访问的数据是否就绪这一问题,进程/线程是否需要等待(在此期间不能做其它事),同步异步是说进程/线程是否需要主动读写数据,同步则是需要主动读写数据,在读写数据的过程中还是会阻塞,异步则是只需要I/O操作完成的通知,进程/线程并不主动读写数据,由操作系统内核完成数据的读写。

二、Unix下的5种IO模型

  • 阻塞式IO
  • 非阻塞式IO
  • IO复用
  • 信号驱动式IO
  • 异步IO

    POSIX把同步IO和异步IO定义如下:

  • 同步IO操作导致请求进程阻塞,直到IO操作完成。
  • 异步IO操作不导致请求进程阻塞。

这五种IO模型里前四种都属于同步IO。

(一)阻塞式IO模型
这里写图片描述

最流行的IO模型是阻塞式IO,默认情形下,所有套接字都是阻塞的。

如图,此处recvfrom视为一个系统调用用以IO操作。当进程调用recvfrom时,内核就开始了IO的第一个阶段:准备数据。对于网络IO来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候内核就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当内核一直等到数据准备好了,它就会将数据从内核中拷贝到用户空间,然后内核返回结果,用户进程才解除阻塞的状态,开始处理数据。
所以,阻塞式 IO的特点就是在IO执行的两个阶段都被block了。这导致用户在发起IO请求时,不能做任何事情,对CPU的资源利用率不够。

(二)非阻塞式IO模型
这里写图片描述

进程把一个套接字设置成非阻塞是在通知内核:当所请求的IO操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。

也就是说,当用户进程发出recvfrom操作时,如果内核中的数据还没有准备好,那么它并不会阻塞用户进程,而是立刻返回一个EWOULDBLOCK错误。从用户进程角度讲 ,它发起一个recvfrom操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送recvfrom操作。一旦某次再次调用recvfrom时内核中的数据准备好了,那么内核马上就将数据拷贝到了用户空间,然后返回。

所以,整个IO请求的过程中,虽然用户线程每次发起IO请求后可以立即返回,但是为了等到数据,仍需要不断地轮询、重复请求,消耗了大量的CPU的资源。

(三)IO复用模型
这里写图片描述

有了IO复用,我们就可以调用select或poll或epoll,阻塞在这三个函数之上,而不是阻塞在真正的IO系统调用上。就拿select来说吧,进程阻塞于select调用 ,此时内核会监视所有select负责的套接字,当任何一个socket中的数据准备好了,select就会返回可读条件,之后用户进程就可以调用recvfrom直接让内核将数据拷贝到用户空间。使用select需要两个而不是单个系统调用,使用select的优势在于可以等待多个描述符就绪。所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

(四)信号驱动式IO模型
这里写图片描述

用户进程首先开启套接字的信号驱动式IO功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,用户进程可以继续做其它的事,也就是说它没有被阻塞。当数据报准备好供读取时,内核就为该进程产生一个SIGIO信号。用户进程随后在信号处理函数中调用recvfrom直接让内核将数据拷贝到用户空间。

(五)异步IO模型
这里写图片描述

用户进程发起aio_read操作之后,立刻就可以开始做其它的事。在这期间,内核会在整个操作(包括等待数据和将数据从内核拷贝到用户空间)完成后给用户进程发送一个信号来通知。这种模型与信号驱动式IO模型的主要区别在于:信号驱动式IO是由内核通知用户进程何时可以启动一个IO操作,而异步IO模型是由内核通知用户进程IO操作何时完成了。

最后贴出5种IO模型的直观比较:
这里写图片描述

三、select函数

select函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。

(一)select函数及参数说明:

#include <sys/select.h>  
#include <sys/time.h>  
int select(int maxfdp1, fd_set *readfds, fd_set *writefds,  
           fd_set *exceptfds, struct timeval *timeout);  
//返回:若有就绪描述符则返回就绪描述符的数目,若超时返回0,若出错返回-1  

1.timeout:告知内核等待指定描述符中任何一个就绪花费的最长时间,其timeval结构用于指定秒数和微妙数。

//timeval结构:  
struct timeval  
{  
    long    tv_sec;     /* seconds */  
    long    tv_usec;    /* microseconds */  
};  

这个参数有以下三种可能:

timeout== NULL:永远等待下去,即仅当一个描述符准备好I/O时才返回。

timeout->tv_sec != 0 || tvptr->tv_usec !=0:等待一段固定的时间,超时返回0;在这段时间内如果有描述符准备好就返回。

timeout->tv_sec == 0 && tvptr->tv_usec == 0:根本不等待,即检查描述符后立即返回,这称为轮询(非阻塞式I/O就是轮询)。

2.中间的三个参数:指定要让内核测试读、写、异常的描述符,若对某一个不感兴趣可置为NULL。

这三个参数都是值-结果参数,调用函数时,用于指定所关心的描述符的值;函数返回时,结果将指示哪些描述符已经就绪。该函数返回后,使用FD_ISSET宏来测试fd_set数据类型中的描述符。描述符集内任何与未就绪描述符对应的位返回时均清为0。为此,每次重新调用select函数时,我们都得再次把所有描述符集内所关心的位均置为1.

select使用描述符集,通常是一个整数数组,其中每个整数的一位对应一个描述符。举例来说,假设使用32位整数,那么该数组的第一个元素对应于描述符0~31,第二个元素对应于描述符32~63,依此类推。所有这些实现细节都与应用程序无关,它们隐藏在为fd_set的数据类型和以下四个宏中:

FD_CLR(int fd, fd_set *set);  //关闭fd_set中的fd位  
FD_ISSET(int fd, fd_set *set); //测试该位是否打开,如果为1则该位对应描述符就绪  
FD_SET(int fd, fd_set *set);  //打开该fd位  
FD_ZERO(fd_set *set);         //清空所有位 

如下打开描述符1、4位:

fd_set rset;  
FD_ZERO(&rset);  //清空所有,每次调用select都要清空为0  
FD_SET(1,&rset);  //打开描述符1 
FD_SET(4,&rset);  //打开描述符4

3.maxfdp1参数指定待测试的描述符的个数,其值为最大待测试描述符加1。例如上例打开1、4描述符,那么这里maxfdp1值为5。

(二)select的特点

1.最大并发数限制:因为一个进程所打开的 FD (文件描述符)是有限制的,由 FD_SETSIZE 设置,默认值是 1024,因此 Select 模型的最大并发数就被相应限制了。如果要改变FD_SIZE的大小需要重新编译内核。

2.效率问题:select 每次调用都会线性扫描全部的 FD 集合,花费时间为O(n),这样效率就会呈现线性下降,即使将 FD_SETSIZE 改大其性能也会很差。
3. 内核/用户空间内存拷贝问题:select 采取了内存拷贝方法让内核把 FD 消息通知给用户空间。

4.事件集:select的参数类型fd_set没有将文件描述符和事件绑定,它仅仅是一个文件描述符的集合,因此select需要提供3个“值-结果”类型的参数分别传入和输出可读、可写和异常等事件(调用该函数时,指定所关心的描述符的值,函数返回时,结果将指示哪些描述符已经就绪),也就是select函数会修改指针readset、writeset、exceptset所指向的描述符集。

一方面,使得select不能处理更多类型的事件,所能处理的事件类型只有读写异常三类;

另一方面,描述符集内任何与未就绪描述符对应的位返回时都会被清空,因此每次重新调用select时,都需要再次把所有的描述符集内所关心的位置为1。

5.select函数的定时是由函数的最后一个参数决定的,它是一个timeval结构体,用于指定这段时间的秒数和微秒数。

四、poll函数

(一)poll函数及其参数说明

#include <poll.h>  
int poll(struct pollfd *fds, nfds_t nfds, int timeout);  
//返回:如有就绪描述符就返回其数目,超时返回0,若出错返回-1.

1.第一个参数是指向一个结构数组第一个元素的指针。每个元素是一个pollfd结构,用于指定测试某个给定描述符fd的条件。

//pollfd结构体  
struct pollfd  
{  
    int   fd;         /* file descriptor :要测试的描述符*/  
    short events;     /* requested events: 要测试的事件 */  
    short revents;    /* returned events :  返回该描述符的状态*/  
}; 

pollfd结构的成员events和revents避免了使用值-结果参数。这点与select不同。

用于指定这两个成员的一些常值如下:
这里写图片描述

2.第二个参数nfds:指定第一个参数结构数组中元素的个数。

3.第三个参数timeout:指定poll函数返回前等待多长时间。
这里写图片描述

INFTIM常值被定义为一个负值。

当发生错误时,poll函数的返回值为-1,若定时器到时之前没有任何描述符就绪,则返回0,否则返回就绪描述符的个数,即revents成员值非0的描述符个数。

如果我们不关心某个特定描述符,那么可以把与它对应的pollfd结构的fd成员设置成一个负值。poll函数将忽略这样的pollfd结构的events成员,返回时将它们的revents成员的值置为0。

(二)poll的特点

1.最大并发数限制:poll的第二个参数nfds是第一个参数指示的结构数据的元素个数,这个nfds并没有select的限制,它只受限于系统的内存空间(可以达到系统所允许打开的最大描述符的个数,即65535)。

2.效率问题:效率和select类似。

3.内核/用户空间内存拷贝问题:和select类似。

4.事件集:poll比select要“聪明”,它将描述符和事件定义在一起,任何事件都被统一处理,编程接口简洁许多。

一方面,poll可以监听的事件类型就可以更细分为很多种。

另一方面,而且内核每次修改的是pollfd结构体的revents成员,而events成员不变,因此下次重新调用poll无需重置pollfd类型中的事件集参数(避免了类似于select使用的的“值-结果”参数)。

5.poll的定时也是由函数的最后一个参数给出,但是它是一个int类型(指定函数要等待的毫秒数),而不是timeval结构体。

五、epoll函数

(一)epoll类的三个函数

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); 

1.int epoll_create(int size);

创建一个epoll的句柄,之后的所有操作将通过这个句柄来进行操作。size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数那样给出最大监听的fd+1的值。自从linux2.6.8之后,size参数是被忽略的,只要它比0大就可以。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

2.int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件&#x

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值