一、函数调用中常用的术语

    函数调用包含I/O调用,

1、阻塞和非阻塞

阻塞: 

   进程发起函数调用时,调用未完成之前,当前进程会被挂起(此时进程为不可中断睡眠状态“D”)


非阻塞:

   进程发起函数调用时,调用未完成之前不会阻塞当前进程而是立即返回(返回的结果是还未完成,你(进程)该干嘛干嘛去,不要在这干等着)

  非阻塞又有两种方式:主动查询被动接收消息

       被动不意味着一定不好,在这里它恰恰是效率更高的,因为在主动查询里绝大部分的查询是在做无用功是盲等待;


   通俗的理解就就好比,我去一家面馆点了一份红烧牛肉面,面馆老板(相当于内核)不可能马上给做好的牛肉面给你吃,通常都叫你做着等会马上就好,此时我可以选择一直坐在桌子上等着老板端面过来,而不去做别的事(如果我去做别的事,面好了我也不知道),这就叫阻塞;我也可以选择去外面先逛逛街,这就叫非阻塞,那如果面好了,我怎么知道了,这里也有两种方式,一种是我在面馆外逛街,每隔一分钟就返回一次到面馆,询问老板我的面是否好了,这就叫主动查询,也可以让面好了时,老板打我电话,通知我去吃面,这就是被动接收消息。


2、同步和异步

同步

   进程发起一个过程调用(过程调用没有返回值)调用后,在没得到结果之前,该调用将不会返回(不会返回的意思是,我点了面,但面馆老板还没做好面之前不理我,直到面做好了,直接端给我)


异步:

   进程发起一个过程调用,即便调用者不能立即得到结果,但调用却会返回,返回时未完成状态,当调用完成后,内核会自行通知调用者已经OK。


   阻塞和非阻塞是调用方式不同,更偏向于程序自己本身,同步和异步是偏向于被调用者如何响应的,更偏向于消息通知机制,但指的都是自身进程是否被阻塞。


二、磁盘I/O

1、磁盘I/O

  我们都知道web服务器的进程响应用户请求,但无法直接操作I/O设备,其必须通过系统调用,请求kernel来协助完成I/O动作        

内核会为每个I/O设备维护一个buffer,如下图:

IO模型

      对于数据输入而言,即等待(wait)数据输入至buffer需要时间,而从buffer复制(copy)数据至进程内存也需要时间,即:  

一次I/O调用分为两个阶段:

  等待数据完成阶段:内核等待,数据从磁盘到内核内存

  等待复制完成阶段:进程等待,数据从内核内存复制到进程内存


2、I/O调用模型

根据等待模式不同,I/O动作可分为五种模式:

 1)同步阻塞

     数据I/O两个阶段都阻塞

 2)同步非阻塞     # 很少见;同步不会做任何消息返回

     第一阶段盲等:需要频繁的回头查看数据是否完成

     第二个阶段仍然阻塞     

 3)I/O复用

     内核提供一个独特的系统调用,专门用来帮助进程监控调用是否完成(可以监控多个调用)

     第一阶段阻塞在多路IO上(需要一直盯着监控),第二阶段仍然阻塞

     通俗来讲就是进程不再直接向内核询问各个调用是否好了,而是查看这个监控,我托管需要你监控的调用是否好了。

     BSD:select()(内生限定并发不能超过1024个),SystemV:poll()

 4)事件驱动

     被调用者调用完成后会通知主动调用者

     通知有两种方式:

        边缘触发:只通知调用者一次

        水平触发:多次通知

     第一阶段不阻塞,第二个阶段仍然阻塞

      linux:epoll(),BSD:wqueue()

     通俗来讲就是面好了,我打电话通知给你:面已经好了但放在前台,需要你自己去端到桌子上再吃    

 5)AIO(异步非阻塞)

    真正的异步,内核完成2个阶段后再通知调用者,2个阶段都不阻塞

    前面通知说调用已经好了,都是针对第一阶段(内核等待数据完成就通知调用者说好了,但第二阶段是需要进程自己去复制数据,仍然阻塞),AIO是二个阶段都好了才通知调用者好了

    一般网络IO不支持异步,只有文件(磁盘)IO支持异步

   通俗来讲就是面好了,我打电话通知给你:面已经好了放在你桌子了,你回来了就可以吃了

 6)mmap(内存映射) # 只读访问,零复制

     数据由磁盘直接以页面形式映射进内核内存中,内核再将数据共享给进程,

 7)Sendfile()

     在内核内存中直接构建响应报文,资源不需要到达用户空间

     Sendfile()支持的文件大小有限,Sendfile64()支持更大的文件直接在内核中构建响应报文


异步阻塞,不存在,为什么?

      我都通知你好了(你该干嘛干嘛去),你还在这干等着,你是不是傻?


3、I/O模型总结

wKiom1jADgTS264yAAGrFVBg3MM251.png从上图中我们可以看出,可以看出,越往后,阻塞越少,理论上效率也是最优


总结:

     不管是I还是O,对磁盘或网卡访问都可以分成请求(第一个等待)执行(第二个等待)两个阶段。请求就是看磁盘或网卡的状态信息(比如是否准备好了),执行才是真正的I/O操作。

      在Linux 2.6之前,只有“请求”是异步事件,2.6之后才引入AIO把“执行”异步化。别看Linux/Unix是用来做服务器的,这点上比Windows落后了好多,IOCP(Windows上的AIO)在Win2000上就有了。

      Linux上的前四种I/O模型的“执行”阶段都是阻塞的,只有最后一种才做到了真正的全异步。第一种同步阻塞式是最原始的方法,也是最累的办法。当然累与不累要看针对谁。应用程序是和内核打交道的。对应用程序来说,这种方式是最累的,但对内核来说这种方式恰恰是最省事的。当然现在计算机的设计,包括操作系统,越来越为终端用户考虑了,为了让用户满意,内核慢慢的承担起越来越多的工作,IO模型的演化也是如此。

  同步非阻塞I/O ,I/O复用,信号驱动式I/O其实都是非阻塞的,当然是针对“请求”这个阶段,同步非阻塞式主动查询网卡或磁盘的状态I/O复用里的select(),poll()是主动查询,不同的是select()和poll可以同时查询多个fd(文件句柄)的状态,另外select()有fd个数不能超过1024个1024限定的不只是监听的个数,还是文件描述符的最大值,注意,是值,是select内生的限制,要超过需要修改内核代码,重新编译)的限制。epoll是基于回调函数的,信号驱动式I/O则是基于信号消息的,这两个应该可以归到“被动接收消息”那一类中。最后就是伟大的AIO的出现,内核把什么事都干了,对上层应用实现了全异步,性能最好,当然复杂度也最高。


三、网络I/O

   并发访问响应模型

单进程模型:

   每个进程只有一个线程,没有进程响应一个请求,串行方式响应

多进程模型

   prefork:一个进程响应一个用户请求,并发使用多个进程实现

多线程模型:

   worker:一个进程生成多个线程,一个线程响应一个请求,并发使用多个线程实现;n个进行,m个线程,那就可以响应n*m个请求 

事件驱动模型:

   event-driven,一线程响应多个用户请求,基于事件驱动机制来维持多个用户请求


四、集群Session持久机制

stateless:http协议是无状态

  无状态:不记录用户的任何信息,无法追踪用户,

  cookie:服务器返回给用户的唯一标识


 1)Session sticky(粘性)  

    基于源IP绑定:lvs sh算法,持久连接

    基于cookie绑定:

    反均衡,服务器故障时session问题解决不了

基于cookie而不再是IP做调度:

   用户在访问服务器时,服务器会给客户端发一个瘦cookie,而后将每个cookie和session的对应关系,保存在一个表中。此后客户端请求时,服务器通过追踪客户端提交的cookie来关联它的session。由于LVS工作在4层而不是应用层,因此不支持。因此haproxy和Nginx便有了用武之地。  

 2)Session Replication Cluster 

    Session复制集群,适用于少数RS(3-5个RS)

   各RS做成多播集群,RS之间以多播方式“复制”各session,从而每个RS会持有所有session

   这个集群缺点在于,后端每台RS都要维护RS数量倍数的会话,RS数量越多,RS的压力就越大。

   缺点:服务器资源、网络资源消耗过大,  

 3)Session Server

   使用独立的服务器,专用于共享存储session信息(redis,memcached)

   session server需要高可用