转载:https://www.cnblogs.com/George1994/p/6702084.html
故事:老王烧开水。
出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。
老王想了想,有好几种等待方式
1.老王用水壶煮水,并且站在那里,不管水开没开,每隔一定时间看看水开了没。-同步阻塞
老王想了想,这种方法不够聪明。
2.老王还是用水壶煮水,不再傻傻的站在那里看水开,跑去寝室上网,但是还是会每隔一段时间过来看看水开了没有,水没有开就走人。-同步非阻塞
老王想了想,现在的方法聪明了些,但是还是不够好。
3.老王这次使用高大上的响水壶来煮水,站在那里,但是不会再每隔一段时间去看水开,而是等水开了,水壶会自动的通知他。-异步阻塞
老王想了想,不会呀,既然水壶可以通知我,那我为什么还要傻傻的站在那里等呢,嗯,得换个方法。
4.老王还是使用响水壶煮水,跑到客厅上网去,等着响水壶自己把水煮熟了以后通知他。-异步非阻塞
老王豁然,这下感觉轻松了很多。
同步和异步
同步就是烧开水,需要自己去轮询(每隔一段时间去看看水开了没),异步就是水开了,然后水壶会通知你水已经开了,你可以回来处理这些开水了。
同步和异步是相对于操作结果来说,会不会等待结果返回。
阻塞和非阻塞
阻塞就是说在煮水的过程中,你不可以去干其他的事情,非阻塞就是在同样的情况下,可以同时去干其他的事情。阻塞和非阻塞是相对于线程是否被阻塞。
其实,这两者存在本质的区别,它们的修饰对象是不同的。阻塞和非阻塞是指进程访问的数据如果尚未就绪,进程是否需要等待,简单说这相当于函数内部的实现区别,也就是未就绪时是直接返回还是等待就绪。
而同步和异步是指访问数据的机制,同步一般指主动请求并等待I/O操作完毕的方式,当数据就绪后在读写的时候必须阻塞,异步则指主动请求数据后便可以继续处理其它任务,随后等待I/O,操作完毕的通知,这可以使进程在数据读写时也不阻塞。
网络IO的模型大致包括下面几种
网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。对于一次IO访问,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间,所以一般会经历两个阶段:
对于socket流而言:
这也是最常用的模型,默认情况下所有的套接字都是 阻塞
的;
我们把recvfrom函数视为系统调用,因为我们正区分进程和内核,系统调用一般都会从在应用进程空间中运行切换到内核空间中运行,一段时间后又再切换回来;
我们可以从图中看到,应用进程从 进行系统调用
到 复制数据报到应用进程的缓冲区完成
的整段时间内是被阻塞的;在这个过程中,要么正确到达,要么系统调用被信号打断;直到数据报被复制到用户进程完成后,用户进程才解除阻塞的状态,当然,这是用户进程自己进行的阻塞;
非阻塞,当所请求的I/O操作非得把当前进程设置成睡眠才能完成时,不要把当前进程设置成睡眠,而是返回一个错误信息(数据报没有准备好的情况下),此时当前进程可以做其它的事情,不用阻塞;
从图中可以得知,前三次系统调用时都没有数据可以返回,内核均返回一个 EWOULDBLOCK
,并且不会阻塞当前进程,直到第四次询问内核缓冲区是否有数据的时候,此时内核缓冲区中已经有一个准备好的数据,因此将内核数据复制到用户空间,此时系统调用则返回成功;
当一个应用进程像这样对一个非阻塞socket循环调用
recv/recvfrom
时,则称为轮询;应用进程持续轮询内核,以查看某个操作是否就绪,这么做往往消耗大量的CPU时间。
优点:相较于阻塞模型,非阻塞不用再等待任务,而是把时间花费到其它任务上,也就是这个当前线程同时处理多个任务;
缺点:导致任务完成的响应延迟增大了,因为每隔一段时间才去执行询问的动作,但是任务可能在两个询问动作的时间间隔内完成,这会导致整体数据吞吐量的降低。
有了I/O复用,我们就可以调用 select或poll
,让其阻塞在两个系统调用(1.询问数据是否准备好并且直到数据准备好才返回;2.内核是否把数据全部复制完成到用户进程)中的某一个之上
图中阻塞于 select
调用,等待数据报套接字变为可读。当select返回套接字可读这一条件的时候,则调用 recvfrom
把所读数据报复制到应用进程缓冲区;
之前的同步非阻塞方式需要用户进程不停的轮询,但是IO多路复用不需要不停的轮询,而是派别人去帮忙循环查询多个任务的完成状态,UNIX/Linux 下的 select、poll、epoll
就是干这个的;select调用是内核级别的,select轮询相对非阻塞的轮询的区别在于---前者可以等待多个socket,能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准好了,就能返回进行可读,然后进程再进行recvform系统调用,将数据由内核拷贝到用户进程,当然这个过程是阻塞的。select或poll调用之后,会阻塞进程,与blocking IO阻塞不同在于,此时的select不是等到socket数据全部到达再处理, 而是有了一部分数据(网络上的数据是分组到达的)就会调用用户进程来处理。如何知道有一部分数据到达了呢?监视的事情交给了内核,内核负责数据到达的处理。
我认为上面那句话中存在两个重要点:1.对多个socket进行监听,只要任何一个socket数据准备好就返回可读;2.不等一个socket数据全部到达再处理,而是一部分socket的数据到达了就通知用户进程;
其实 select、poll、epoll
的原理就是不断的遍历所负责的所有的socket完成状态,当某个socket有数据到达了,就返回可读并通知用户进程来处理;
高并发的程序一般使用同步非阻塞方式而非多线程 + 同步阻塞方式。要理解这一点,首先要扯到并发和并行的区别。比如去某部门办事需要依次去几个窗口,办事大厅里的人数就是并发数,而窗口个数就是并行度。也就是说并发数是指同时进行的任务数(如同时服务的 HTTP 请求),而并行数是可以同时工作的物理资源数量(如 CPU 核数)。通过合理调度任务的不同阶段,并发数可以远远大于并行度,这就是区区几个 CPU 可以支持上万个用户并发请求的奥秘。在这种高并发的情况下,为每个任务(用户请求)创建一个进程或线程的开销非常大。而同步非阻塞方式可以把多个 IO 请求丢到后台去,这就可以在一个进程里服务大量的并发 IO 请求。
首先开启套接字的信号驱动式IO功能,并且通过 sigaction
系统调用安装一个信号处理函数,该函数调用将立即返回,当前进程没有被阻塞,继续工作;当数据报准备好的时候,内核则为该进程产生 SIGIO
的信号,随后既可以在信号处理函数中调用 recvfrom
读取数据报,并且通知主循环数据已经准备好等待处理,也可以通知主循环让它读取数据报;(其实就是一个待读取的通知和待处理的通知);
我们调用 aio_read
函数,给内核传递描述符、缓冲区指针、缓冲区大小和文件偏移,并且告诉内核当整个操作完成时如何通知我们。该函数调用后立即返回,不被阻塞;
推荐文章
IO多路复用机制详解
聊聊同步、异步、阻塞和非阻塞
聊聊Linux 五种IO模型
聊聊IO多路复用之select、poll、epoll详解