I/O:
IO:一方能够提供服务(被调用方),一方需要调用别人的服务(调用方)。IO请求就是调用方向被调用方请求运行一个应用或函数(库调用,系统调用),被调用方在本地进行一些处理,处理完成后将处理得结果响应给调用方,调用方什么时候知道自己的请求结束了呢?所以就有了同步和异步
同步和异步(synchronous,asyncronous) : 关注的是消息通知机制
同步:调用发出之后,不会立即返回,但一旦返回,则返回最终结果; 异步:调用发出后,被调用方立即返回消息,但返回的并非最终结果,服务端通过状态,通知机制等来通知调用者,或者通过回调函数来实现(发出请求后,对方立即告诉你请求已受到,请等通知)
举个栗子:
你去银行办理业务,银行给你一个号码,你等着被叫号,这就是立即返回结果,但不是最终结果。等到对方叫你的号告诉你轮到你了,然后你再跑过去拿到最终的结果。
阻塞和非阻塞(block,noblock):调用者等待被调用方返回结果时的状态
阻塞:调用结果返回之前,调用者会被挂起,调用者在得到返回结果才能继续 非阻塞:调用者在结果返回之前不会被挂起,即调用不会阻塞调用者。
举个栗子:
去面馆吃面,人很多,你说:老板来碗面,老板说:好,请稍等,
这时候你就坐下来等面了,这时候你啥时都不能干(阻塞)
还有一种情况,你和老板比较熟,可以刷脸了,点完面后你就出去干其他事了,估摸着面差不多已经好了,然后你就回来了(非阻塞)
I/O模型:
-
IO调用:
-
进程向内核发起IO请求时,内核自己没有数据,然后内核把数据从磁盘加载至内核内存中,然后将数据copy一份至进程内存中
一次io调用需要等待两个阶段(一次read请求):
(1) 数据从磁盘到内核内存中
(2) 数据从内核内存到进程内存中
但真正被称为io的仅仅是数据从内核内存到进程内存(因为第一阶段仅仅是内核自处理数据的过程)
如果进程有多个,则进程内存一定是某个进程自己的内存空间,进程不能共享这些数据。
阻塞式IO(blocking IO):
当调用者发出调用请求后,在整个read过程中都会被挂起。处于不可中断睡眠状态。
举个栗子:
用户请求web服务器,则web服务器上有一个进程响应用户请求(假如基于prefork模型:主进程生成多个子进程,每个子进程响应一个用户请求),子进程发现用户请求一个资源,于是乎子进程向内核发起调用请求加载用户所请求的页面资源,在此过程中该子进程不响应别的用户请求(该子进程已经被挂起了)。
这只是一种假设,真正的prefork模型不工作在阻塞io下,因为进程要处理两种io,网络io(接收用户请求),磁盘io。
非阻塞式IO(noblocking IO):存在忙盲等待
买完面出去玩了。但是你怎么知道什么时候面好了呢?老板说我们不负责通知,这时候你要怎么办呢?为了及时能吃到面,所以就不得不每隔一段时间去面馆里看看,没好,出去玩,过一会再看,还没好,再出去玩…….,这种就是盲等待,你得一遍又一遍的来问,其实这种效果不一定比阻塞好,一旦老板告诉你面ok了,其实是面放在柜台上了,你想吃,还得自己去端(即数据已经从磁盘到内核内存了,还没有从内核内存到进程内存),在你端面得过程中依然是阻塞的,你依然干不了其他事。
IO 复用 (IO multiplexing):调用者将请求提交给代理人,代理人再将请求提交给被调用者,select(),poll()。(这种实质上也有阻塞,只不过阻塞不是阻塞在自己直接发出的io上,而是阻塞在助理上了。)
任何进程自身一般只能处理一个IO,因为它一旦被某个IO阻塞,直到阻塞被唤醒之前,其他人的事情它时不知道的。但作为一个web服务器来讲,一个进程既要接受用户请求(网络IO),还要进行磁盘IO。一旦进程被阻塞在磁盘io上,这时候网络io发生异动,该进程就不知道了。(例如进程在磁盘上加载数据了,加载到一半,你觉得太慢,直接Ctrl + C了,但是如果这个进程被阻塞在磁盘io上,这是一个不可中断睡眠,你的Ctrl C这个进程收不到,但是在现实生活中,Ctrl C是可以被响应的,这是为什么呢?这就是多路IO的机制了)
有人在内核中开发了一种多路io(复用型io)的调用程序,当调用者需要进行io调用时,他会将自己的请求扔给内核提供的代理人,代理人转为内核能够理解的请求,这样一来,用户请求会被阻塞在代理人上,而非内核中真正要完成的任务上,举个栗子:
银行柜台一次只能处理一个用户,那就有好多用户在排队等待,有个用户请求开一张银行卡,在办理银行卡的过程中该客户只能一直等待着,那么就说这个人被阻塞在他所调用的服务上。
假设还是办理银行卡的业务,每个柜台人员配有一名助理,该助理是面对于客户的,客户讲自己的请求告诉给助理,助理负责帮你送给柜台工作人员,假设有两个柜台,分别可以处理不同的用户请求,该客户既要办理银行卡,还要存款,客户将办卡的请求扔给助理后,助理再扔给工作人员,这时候助理就闲下来了,这时候客户又告诉助理我要存款,这时候助理就把存款请求扔个另一个柜台了!
所以我们可以把调用程序想象成助理,虽然这个调用依然是阻塞的,但是助理却可以帮我们实现多路请求,即便第一路被阻塞了,也不影响我们第二路继续接受和处理用户请求。这种调用就是复用型io,通过上述例子我们可以知道复用型io“助理”是关键,我们必须得有一个支持两路以上的助理,这个助理在内核中就是一种特殊的系统调用,称为select事件驱动型IO():被调用者收到调用者的请求后直接告诉调用者请求我已经收到,你现在可以干你的事情(非阻塞),等待请求完成我会通知你,但是调用者从被调用方拿数据的过程依然不能干其他事(阻塞)。
web服务器上有一个进程,第一个用户请求过来了,进程把用户请求给内核,内核说好了,我知道了,你现在该干嘛就干嘛去,这时候这个进程就闲下来了,可以继续接收其他请求了。当内核发现数据从磁盘已经加载到内核内存中了,会通知你,这时候数据从内核内存加载到进程内存这个过程中仍是阻塞的,仍然干不了其他事。
通知:
(1)边缘触发:只通知一次;
(2)水平通知:多次通知(存在资源浪费)异步IO:完全非阻塞
调用者向内核发起请求,内核说请求已收到,该干嘛干嘛去,这时候内核在后台默默的完成第一步,默默的完成第二步。当做完所有事情后告诉你已经ok了,这时候进程立即将数据打包成响应报文响应给客户端。(内核会帮你把饭端到你之前定好的位置上)
prefork,worker用的是io复用
event用的是事件驱动式io
prefock(进程):主进程生成多个子进程,每个子进程处理一个请求
worker(线程):主进程生成多个子进程,子进程生成多个线程,每个线程响应一个请求
envent:主进程生成多个子进程,每个子进程响应多个请求;