这里把recvfrom函数视为系统调用,一般而言,调用过程都会在应用进程空间切换到内核空间,一段时间后再切换回来。进程调用recvfrom,其系统调用指导数据报到达企鹅杯复制到应用进程的缓冲区中或者发生错误才返回。进程从调用recvfrom开始刀塔返回的整段时间内是被阻塞的。recvfrom成功返回后,应用进程开始处理数据报。模型如下图
进程可以把一个套接字设置成非阻塞,这是在告诉内核,如果所请求的I/O操作非得把本进程的状态更改成睡眠(就绪?)才能完成时,不要本进程投入睡眠,而是返回一个错误。模型如下图
上图中,前三次调用recvfrom是没有数据可以返回,因此内核立即返回一个EWOULDBLOCK错误。第四次调用recvfrom时已经有一个数据报准备好,他被复制应用进程缓冲区,于是revcfrom成功返回。应用进程继续处理数据。
当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom时,我们称之为轮询(polling)。应用进程持续轮询内黑,以检查某个操作是否就绪。这么做往往耗费大量的CPU时间。
有了IO复用,我们可以调用select或poll,阻塞在这两个系统调用的某一个之上,而不是阻塞在真正的IO系统调用上。
我们阻塞与select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom吧所读取的数据复制到应用进程缓冲区。
IO复用需要执行两个系统调用,一个用来处理真正的IO调用,另一个用来监测其状态。看似这并没有什么优势。不过在下文我们将会看到,使用select的优势在于我们可以等待多个描述符就绪。
IO调用时也可以使用信号,让内黑在描述符就绪是发送SIGIO信号通知我们。我们称这种模型为信号驱动式IO,如下图
执行过程为,首先开启套接字的信号驱动式IO功能,并通过sigaction系统调用安装信号处理函数。该系统调用将立即返回,我们的进程继续执行。也就是说在这一部分没有阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号,我们随后既可以在信号处理函数中调用recvfrom读取数据报,将数据从内核复制到用户缓冲区。数据从内核中复制到缓冲区的过程是被阻塞的。
简单讲其工作机制是:告知内核启动某个操作,并让内核在整个操作(包括内核到用户缓冲区的复制)完成后通知我们。可以发现其与信号驱动式的区别是,前者在IO操作何时完成通知我们,后者通知我们何时可以启动一个IO操作。细节如下图
这里我们对同步和异步下一个定义
- 同步IO 导致请求进程阻塞,知道IO操作完成
- 异步IO 不导致请求进程阻塞
可以发现前除了最后一种异步IO模型,其他四种都会导致进程阻塞,所以他们都是同步IO操作
对于给定的描述符进行轮询,当有描述符就绪时返回,返回值是就绪描述符的数目,select返回后,用户程序需要遍历确定给定的描述符中哪一个已经就绪,从而执行后面的操作。select模型每次调用都会讲描述符的内容(一个数组)从用户空间拷贝到内核空间,当数量较大时,效率会很低。 此外,select会轮询遍历所有的关注的描述符,描述符数量较大时效率也会很低。
内部实现与select基本相同,只是传入参数的方式不同,而且会把就绪的描述符放置在一个参数中返回,用户程序可以遍历这个数组以得到就绪的描述符
将用户关心的文件描述符的时间存放在内核中的一个事件表中,用户空间和内核空间只需要复制一次。当某个描述符就绪时,内核会采用类似callback的回调机制(软件中断)迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知