Linux下套接字具体解释(三)----几种套接字I/O模型

时间:2023-02-08 22:37:31

參考:

网络编程–IO模型演示样例

几种server端IO模型的简介及实现

背景知识


堵塞和非堵塞


对于一个套接字的 I/O通信,它会涉及到两个系统对象。一个是调用这个IO的进程或者线程,还有一个就是系统内核。

比方当一个读操作发生时。它会经历两个阶段:

①等待数据准备 (Waiting for the data to be ready)

②将数据从内核复制到进程中 (Copying the data from the kernel to the process)

堵塞,在linux中,默认情况下全部的socket都是blocking,当用户进程调用了recvfrom/recv这个系统调用,啮合就開始了IO的第一个阶段:准备数据。

可是非常多时候数据在一開始还没有到达(比方,还没有收到一个完整的UDP/TCP包),这个时候内核就要等待足够的数据到来。

而在用户进程这边,整个进程会被堵塞。当内核一直等到数据准备好了,它就会将数据从内核中复制到用户内存。然后直到返回结果。用户进程才解除堵塞的状态。又一次运行起来。

所以。堵塞IO的特点就是在IO运行的两个阶段都被堵塞了。

调用堵塞IO会一直堵塞相应的进程直到操作完毕,而非堵塞IO在内核还准备数据的情况下会立马返回。

堵塞

因此我们给出堵塞的简单定义,堵塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可运行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数仅仅有在得到结果之后才会返回。

有人也许会把堵塞调用和同步调用等同起来。实际上他是不同的。对于同步调用来说,非常多时候当前线程还是激活的,仅仅是从逻辑上当前函数没有返回而已。

比如,我们在socket中调用recv函数,假设缓冲区中没有数据,这个函数就会一直等待。直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息,所以我们应该说我们的线程如今是堵塞的。

快递的样例:比方到你某个时候到A楼一层(假如是内核缓冲区)取快递。可是你不知道快递什么时候过来,你又不能干别的事,仅仅能死等着。

但你能够睡觉(进程处于休眠状态)。由于你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。

非堵塞

非堵塞和堵塞的概念相相应。指在不能立马得到结果之前,该函数不会堵塞当前线程,而会立马返回。

还是等快递的样例:假设用忙轮询的方法。每隔5分钟到A楼一层(内核缓冲区)去看快递来了没有。

假设没来,马上返回。而快递来了,就放在A楼一层。等你去取。

对象的堵塞模式和堵塞函数调用

对象是否处于堵塞模式和函数是不是堵塞调用有非常强的相关性,可是并非一一相应的。堵塞对象上能够有非堵塞的调用方式。我们能够通过一定的API去轮询状 态,在适当的时候调用堵塞函数,就能够避免堵塞。而对于非堵塞对象。调用特殊的函数也能够进入堵塞调用。我们经常使用的函数select就是这样的一个样例。

同步与异步


首先我们给出POSIX的定义

  A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;

同步

用我们的话说。所谓同步,就是在发出一个功能调用时。在没有得到结果之前,该调用就不返回。

也就是必须一件一件事做,等前一件做完了才干做下一件事。而仅仅有当全部的工作依照顺序运行完之后,才会返回调用的位置。继续往下运行。

这个非常好理解,我们一般做的函数调用,都是同步的,我们的程序依照我们既定的顺序一步一步运行。在前一个操作返回后,我们依据操作的结果。进行下一个阶段的处理,这就是一个同步的过程。

异步

异步。异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立马得到结果。实际处理这个调用的部件在完毕后,通过状态、通知和回调来通知调用者。

简单来说

一个同步接收的方式,在这个port下假设同是来了两个client请求,第一个连接得到响应,与服务端建立通讯。而第二个请求就会被一直堵塞直到第一个请求完毕操作。各个请求之间就好像排个队,顺序运行,这就是同步。

而一个异步接收方式,就是同一时候来两个或者多个请求,服务端就同一时候响应多个client。同一时候给他们连接。

各个client与server的通讯是并行的,一个client不必等还有一个client完毕操作。

两者的差别就在于同步IO做IO操作的时候会将process堵塞

事实上堵塞IO,非堵塞IO,以及我们后面会提到IO复用都属于同步 IO。有人可能会说。非堵塞IO并没有被堵塞啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作。比方非堵塞IO在运行recvfrom这个系统调用的时候,假设内核的数据没有准备好。这时候不会堵塞进程。可是。当内核中数据准备好的时候,recvfrom会将数据从内核复制到用户内存中,在这段时间内,进程是被堵塞的。而异步IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到内核发送一个信号,告诉进程说IO完毕。在这整个过程中,进程全然没有被堵塞。

我们能够总结为一下几点

同步 就是我调用一个功能,该功能没有结束前,我死等结果。

仅仅能顺序运行。

异步 就是我调用一个功能,不须要知道该功能结果,该功能有结果后通知我(回调通知)。往往用回调函数或者其它类方式实现。

堵塞 就是调用我(函数),我(函数)没有接收完数据或者没有得到结果之前。我不会返回。

非堵塞 就是调用我(函数),我(函数)马上返回。而当我准备完毕时,通过select通知调用者。

同步IO和异步IO的差别就在于:数据拷贝的时候进程能否够被堵塞

堵塞IO和非堵塞IO的差别就在于:应用程序的调用能否马上返回

并发与迭代


套接字编程经常使用在客户/server编程模型(简称C/S模型)中。C/S模型依据复杂度分为简单的客户/server模型复杂的客户/server模型

C/S简单客户/server模型是一对一关系。一个server端某一时间段内仅仅相应处理一个client的请求,迭代server模型属于此模型。

C/S复杂server模型是一对多关系,一个server端某一时间段内相应处理多个client的请求,并发server模型属于此模型。

迭代server模型并发server模型是socket编程中最常见使用的两种编程模型。

通常来说大多数TCPserver是并发的,大多数UDPserver是迭代的

比方我们通常在做聊天室的时候,使用UDP协议来发送消息,可是在用户须要上传下载数据的时候,我们通常在server和client之间建立一个TCP连接来传送文件。。。

可是假设服务一个客户请求的时间不长。使用迭代server没有太大问题。一旦客户请求的时间须要花费非常长,不希望整个server被单个客户长期占用,而希望同一时候服务多个客户,就须要选择并发server了。

Linux下套接字具体解释(三)----几种套接字I/O模型

迭代server逻辑

创建套接字listenfd = socket( ... );
命名套接字bind(listenfd, ... );
開始监听client的连接listen(listenfd, LISTEN_QUENE);
while( 1 ) /* 循环处理每一个client的处理请求 */
{
connfd = serveracceptclient的连接accept(listenfd, ***)
逻辑处理DealLogic(connfd);
在这个connfd上给client发送消息SEND/RECV
关闭close(connfd)
}

这个进程是一个一个处理各个client发来的连接的,比方一个client发来一个连接,那么仅仅要它还没有完毕自己的任务,那么它就一直会占用server的进程直到处理完毕后server关闭掉这个socket。

单进程模式下,假设没有client到来。进程一直堵塞在 accept调用上。堵塞在accept()不可怕。假设堵塞在read()系统调用。将导致整个server不能对其它的client提供服务

[血的教训]

曾经我在编写一个TCP实现文件上传和下载功能的套接字程序时,希望在一次连接中。直接上传和下载文件。终于子上传完毕后,server堵塞在recv函数中,导致下载一直没有完毕,调试怎么也没有发现问题,后来Ctrl+C直接终止了client程序后。server才从瘫痪中苏醒。

或者我们也能够假设这样一个情景:

accept()得到一个client的连接,此时的fd唯一标示该连接。

如今server进入read()系统调用。可是此时的client并没有发送数据,那么服务端一直堵塞在read系统调用。此时来了一个新的连接,可是服务端不能予以相应。就是accept()函数不能被server调用。那么这个连接是失败的。可想而知,迭代类型的server模型是有多么的低效。

这点问题在UDP程序中也和明显,比如只是,UDP恰恰经常使用这样的方式,由于UDP 是非面向连接的。整个过程就是两个函数

recvfrom( );`
DEAL( );
sendto( );

由于UDP 的非面向连接性,并且採用的是非可靠传输,传输速率较TCP快。

假设在传输过程中数据丢失。会导致client堵塞在recvfrom()调用上。

因此最好须要设置client的recvfrom()超时。

。。。

并发server则是相似以下的逻辑

listenfd = socket( ... );    /*  创建套接字  */
bind(listenfd, ... ); /* 命名套接字 */
listen(listenfd,LISTENQ); /* 開始监听套机字 */
while( 1 ) /* 为每一个client请求创建线程或者进程处理 */
{
connfd = accept(listenfd, ... );/* 获取client的连接请求 */
if((pid = fork()) == 0) /* 在子进程中处理client的请求 */
{
close(listenfd); /* 首先关闭掉监听套接字 */
/* 由于子进程并不须要监听,它仅仅负责处理逻辑并发消息给client*/
DealLogic(connfd); /* 处理client请求 */
close(connfd); /* 关闭client的连接套接字 */ }
close(connfd); /* 父进程中关闭client的连接套接字 */
}

这样每来一个client,server就fork(分叉)一个进程去处理请求,这样主进程就一直处于监听状态而不会被堵塞。

千万不要以为fork出来一个子进程就产生了2个新的socket描写叙述符,实际上子进程和父进程是共享listenfd和connfd的,每一个文件和套接字都有一个引用计数。引用计数在文件表项中维护,它是当前打开着的引用该文件或套接字的描写叙述符的个数。

socket返回后与listenfd关联的文件表项的引用计数为1。accept返回后与connfd关联的文件表项的引用计数也为1。然后fork返回后,这两个描写叙述符就在父进程与子进程间共享(也就是被复制),因此与这两个套接字相关联的文件表项各自的訪问计数值均为2。这么一来,当父进程关闭connfd时,它仅仅是把相应的引用计数值从2减为1。该套接字真正的清理和资源释放要等到其引用计数值到达0时才发生。

这会在稍后子进程也关闭connfd时发生。

因此当父进程关闭connfd的时候它仅仅是把这个connfd的訪问计数值减了1而已,由于訪问计数值还 > 0(由于还有client的connfd连着呢),所以它并没有断开和client的连接。

那么假设父进程不关闭connfd有什么后果?

第一。由于可分配的socket描写叙述符是有限的,假设分配了以后不释放,自然内核不会对它回收再利用,那么有限个描写叙述符耗总会有耗尽的一天。

第二,server在将获取client的连接后,将与client通信的任务交给子进程。而父进程期望你能继续监听并accept下一个连接了,但假设每获取一个链接。父进程不关闭自己跟客户的连接。那么这个连接会永远存在!即server获取到的全部client连接都不会断开,始终存在与server的生命周期中。那后果可想而知。。。。

常见的I/O模型

常见网络IO模型有例如以下几类:

堵塞式IO

非堵塞式IO

IO复用

信号驱动IO

异步IO

除了这几个经典的模型之外。还有其它的比方

多进程或者多线程并发I/O, 基于事件驱动的server模型和多线程的server模型(Multi-Thread),以及windows下的IOCP模型和linux下的epoll模型

堵塞I/O(blocking I/O)


简介:进程会一直堵塞。直到数据拷贝完毕,堵塞IO的特点就是在IO运行的两个阶段(等待数据和拷贝数据两个阶段)都被block了

应用程序调用一个IO函数,导致应用程序堵塞,等待数据准备好。 假设数据没有准备好。一直等待….数据准备好了。从内核复制到用户空间,IO函数返回成功指示。

差点儿全部的程序猿第一次接触到的网络编程都是从listen()、send()、recv() 等接口開始的。这些接口都是堵塞型的。

使用这些接口能够非常方便的构建server/客户机的模型。以下是一个简单地“一问一答”的server。

Linux下套接字具体解释(三)----几种套接字I/O模型

套接字在调用recv()/recvfrom()函数时,发生在内核中等待数据和复制数据的过程。调用recv()函数时,系统首先查是否有准备好的数据。

假设数据没有准备好。那么系统就处于等待状态。

当数据准备好后,将数据从系统缓冲区复制到用户空间,然后该函数返回。在套接应用程序中。当调用recv()函数时。未必用户空间就已经存在数据。那么此时recv()函数就会处于等待状态。

当使用socket()函数创建套接字时。默认的套接字都是堵塞的。这意味着当调用Sockets API不能马上完毕时,线程处于等待状态。直到操作完毕。

可是并非全部Sockets API以堵塞套接字为參数调用都会发生堵塞。比如,以堵塞模式的套接字为參数调用bind()、listen()函数时,函数会马上返回。将可能堵塞套接字的Sockets API调用分为以下四种:

1.输入操作: recv()、recvfrom()函数。以堵塞套接字为參数调用该函数接收数据。假设此时套接字缓冲区内没有数据可读,则调用线程在数据到来前一直睡眠。

2.输出操作: send()、sendto()函数。以堵塞套接字为參数调用该函数发送数据。

假设套接字缓冲区没有可用空间,线程会一直睡眠,直到有空间。

3.接受连接:accept()。以堵塞套接字为參数调用该函数,等待接受对方的连接请求。假设此时没有连接请求。线程就会进入睡眠状态。

4.外出连接:connect()函数。对于TCP连接。client以堵塞套接字为參数。调用该函数向server发起连接。该函数在收到server的应答前。不会返回。

这意味着TCP连接总会等待至少到server的一次往返时间。

使用堵塞模式的套接字。开发网络程序比較简单。easy实现。当希望能够马上发送和接收数据,且处理的套接字数量比較少的情况下,使用堵塞模式来开发网络程序比較合适。

堵塞模式套接字的不足表现为。在大量建立好的套接字线程之间进行通信时比較困难。当使用“生产者-消费者”模型开发网络程序时,为每一个套接字都分别分配一个读线程、一个处理数据线程和一个用于同步的事件,那么这样无疑加大系统的开销。其最大的缺点是当希望同一时候处理大量套接字时,将无从下手,其扩展性非常差.

同一时候我们注意到,大部分的socket接口都是堵塞型的。

所谓堵塞型接口是指系统调用(通常是IO接口)不返回调用结果并让当前线程一直堵塞。仅仅有当该系统调用获得结果或者超时出错时才返回。

实际上。除非特别指定。差点儿全部的IO接口 ( 包含socket接口 ) 都是堵塞型的。这给网络编程带来了一个非常大的问题。如在调用send()的同一时候,线程将被堵塞,在此期间,线程将无法运行不论什么运算或响应不论什么的网络请求。

堵塞模式给网络编程带来了一个非常大的问题,如在调用 send()的同一时候,线程将被堵塞,在此期间,线程将无法运行不论什么运算或响应不论什么的网络请求。即我们当前描写叙述的堵塞IOserver模型事实上是一个(同步 + 堵塞 + 迭代 类型的server模式)。这样的server是最低效的模型。给多客户机、多业务逻辑的网络编程带来了挑战。

这时,我们可能会选择多线程的方式(即同步+堵塞+并发)来解决问题。

多线程/进程server(同步 + 堵塞 + 并发)

一个简单的改进方案是在server端使用多线程(或多进程)。多线程(或多进程)的目的是让每一个连接都拥有独立的线程(或进程),这样不论什么一个连接的堵塞都不会影响其它的连接。详细使用多进程还是多线程,并没有一个特定的模式。

传统意义上,进程的开销要远远大于线程,所以假设须要同一时候为较多的客户机提供服务,则不推荐使用多进程;假设单个服务运行体须要消耗较多的CPU资源,譬如须要进行大规模或长时间的数据运算或文件訪问。则进程较为安全。通常,使用pthread_create ()创建新线程,fork()创建新进程。

这样的方式本质上仍然是堵塞I/O,可是使用了多进程或者多线程的I/O来实现并发操作

详细使用多进程还是多线程,并没有一个特定的模式。传统意义上,进程的开销要远远大于线程,所以。假设须要同一时候为较多的客户机提供服务,则不推荐使用多进程;假设单个服务运行体须要消耗较多的 CPU 资源。譬如须要进行大规模或长时间的数据运算或文件訪问,则进程较为安全。通常,使用 pthread_create () 创建新线程,fork() 创建新进程。

多线程/进程server同一时候为多个客户机提供应答服务,主线程持续等待client的连接请求,假设有连接。则创建新线程。并在新线程中提供为前例相同的问答服务。

server套接字每次accept()能够返回一个新的socket。当server运行完bind()和listen()后,操作系统已经開始在指定的port处监听全部的连接请求。假设有请求。则将该连接请求加入请求队列。调用accept()接口正是从 socket s 的请求队列抽取第一个连接信息,创建一个与s同类的新的socket返回句柄。

新的socket句柄即是兴许read()和recv()的输入參数。假设请求队列当前没有请求。则accept() 将进入堵塞状态直到有请求进入队列。

上述多线程的server模型似乎完美的攻克了为多个客户机提供问答服务的要求,但事实上并不尽然。

假设要同一时候响应成百上千路的连接请求。则不管多线程还是多进程都会严重占领系统资源,减少系统对外界响应效率,而线程与进程本身也更easy进入假死状态。

由此 非常多程序猿可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率。其维持一定合理数量的线程。并让空暇的线程又一次承担新的运行任务。

“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。

这两种技术都能够非常好的减少系统开销,都被广泛应用非常多大型系统,如websphere、tomcat和各种数据库等。可是,“线程池”和“连接池”技术也仅仅是在一定程度上缓解了频繁调用IO接口带来的资源占用。并且,所谓“池”始终有其上限,当请求大大超过上限时。“池”构成的系统对外界的响应并不比没有池的时候效果好多少。

所以使用“池”必须考虑其面临的响应规模,并依据响应规模调整“池”的大小。

可是,“线程池”和“连接池”技术也仅仅是在一定程度上缓解了频繁调用 IO 接口带来的资源占用。

并且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并依据响应规模调整“池”的大小。

相应上例中的所面临的可能同一时候出现的上千甚至上万次的client请求。“线程池”或“连接池”也许能够缓解部分压力,可是不能解决全部问题。

非堵塞I/O (nonblocking I/O)


在这样的模型中,当用户进程发出read操作时。假设kernel中的数据还没有准备好,那么它并不会block用户进程。而是立马返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不须要等待。而是马上就得到了一个结果。

用户进程推断结果是一个error时。它就知道数据还没有准备好,于是它能够再次发送read操作。

一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call。那么它马上就将数据复制到了用户内存,然后返回。

所以,在非堵塞式IO中,用户进程事实上是须要不断的主动询问kernel数据准备好了没有。

非堵塞的接口相比于堵塞型接口的显著差异在于,在被调用之后马上返回。linux下使用例如以下的函数能够将某句柄fd设为非堵塞状态。

    fcntl( fd, F_SETFL, O_NONBLOCK ); 

从应用程序的角度来说,blocking read 调用会延续非常长时间。在内核运行读操作和其它工作时,应用程序会被堵塞。

非堵塞的IO可能并不会马上满足,须要应用程序调用很多次来等待操作完毕。这可能效率不高。由于在非常多情况下,当内核运行这个命令时,应用程序必须要进行忙碌等待,直到数据可用为止。

还有一个问题是,在循环调用非堵塞IO的时候,将大幅度占用CPU,所以一般使用select等来检測”能否够操作“。

相同在非堵塞状态下,recv() 接口在被调用后马上返回。而返回值代表了不同的含义。

lag=fcntl(sockfd,F_GETFL,0);

fcntl(sockfd,F_SETFL,flag|O_NONBLOCK)

非堵塞式I/O模型对4种I/O操作返回的错误

读操作:接收缓冲区无数据时返回EWOULDBLOCK

写操作:发送缓冲区无空间时返回EWOULDBLOCK;空间不够时部分拷贝,返回实际拷贝字节数

建立连接:启动3次握手。立马返回错误EINPROGRESS。serverclient在同一主机上connect马上返回成功

接受连接:没有新连接返回EWOULDBLOCK

Linux下套接字具体解释(三)----几种套接字I/O模型

能够看到server线程能够通过循环调用recv()接口,能够在单个线程内实现对全部连接的数据接收工作。可是上述模型绝不被推荐。

由于,循环调用recv()将大幅度推高CPU 占用率。此外。在这个方法中recv()很多其它的是起到检測“操作是否完毕”的作用,实际操作系统提供了更为高效的检測“操作是否完毕“作用的接口。比如select()多路复用模式,能够一次检測多个连接是否活跃。

I/O复用(select 和poll) (I/O multiplexing)


IO multiplexing这个词可能有点陌生,可是假设我说select/epoll,大概就都能明白了。有些地方也称这样的IO方式为事件驱动IO(event driven IO)。我们都知道。select/epoll的优点就在于单个process就能够同一时候处理多个网络连接的IO。

它的基本原理就是select/epoll这个function会不断的轮询所负责的全部socket,当某个socket有数据到达了,就通知用户进程。

它的流程如图:

Linux下套接字具体解释(三)----几种套接字I/O模型

当用户进程调用了select。那么整个进程会被block,而同一时候,kernel会“监视”全部select负责的socket,当不论什么一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作。将数据从kernel复制到用户进程。

这个图和blocking IO的图事实上并没有太大的不同,事实上还更差一些。

由于这里须要使用两个系统调用(select和recvfrom)。而blocking IO仅仅调用了一个系统调用(recvfrom)。可是。用select的优势在于它能够同一时候处理多个connection。所以,假设处理的连接数不是非常高的话。使用select/epoll的server不一定比使用multi-threading + blocking IO的server性能更好,可能延迟还更大。select/epoll的优势并非对于单个连接能处理得更快,而是在于能处理很多其它的连接。

在多路复用模型中。对于每一个socket,一般都设置成为non-blocking。可是。如上图所看到的。整个用户的process事实上是一直被block的。仅仅只是process是被select这个函数block,而不是被socket IO给block。因此select()与非堵塞IO相似。

大部分Unix/Linux都支持select函数。该函数用于探測多个文件句柄的状态变化。

以下给出select接口的原型:



FD_ZERO(int fd, fd_set* fds)

FD_SET(int fd, fd_set* fds)

FD_ISSET(int fd, fd_set* fds)

FD_CLR(int fd, fd_set* fds)

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,

struct timeval *timeout)



这里,fd_set 类型能够简单的理解为按 bit 位标记句柄的队列。比如要在某 fd_set 中标记一个值为16的句柄,则该fd_set的第16个bit位被标记为1。详细的置位、验证可使用 FD_SET、FD_ISSET等宏实现。

在select()函数中。readfds、writefds和exceptfds同一时候作为输入參数和输出參数。

假设输入的readfds标记了16号句柄,则select()将检測16号句柄是否可读。在select()返回后。能够通过检查readfds有否标记16号句柄。来推断该“可读”事件是否发生。另外。用户能够设置timeout时间。

该模型仅仅是描写叙述了使用select()接口同一时候从多个client接收数据的过程。由于select()接口能够同一时候对多个句柄进行读状态、写状态和错误状态的探測。所以能够非常easy构建为多个client提供独立问答服务的server系统。

例如以下图。

这里须要指出的是,client的一个 connect() 操作,将在server端激发一个“可读事件”,所以 select() 也能探測来自client的 connect() 行为。

上述模型中,最关键的地方是怎样动态维护select()的三个參数readfds、writefds和exceptfds。

作为输入參数。readfds应该标记全部的须要探測的“可读事件”的句柄,当中永远包含那个探測 connect() 的那个“母”句柄。同一时候。writefds 和 exceptfds 应该标记全部须要探測的“可写事件”和“错误

事件”的句柄 ( 使用 FD_SET() 标记 )。

作为输出參数。readfds、writefds和exceptfds中的保存了 select() 捕捉到的全部事件的句柄值。程序猿须要检查的全部的标记位 ( 使用FD_ISSET()检查 )。以确定究竟哪些句柄发生了事件。

上述模型主要模拟的是“一问一答”的服务流程。所以假设select()发现某句柄捕捉到了“可读事件”。server程序应及时做recv()操作。并依据接收到的数据准备好待发送数据,并将相应的句柄值加入writefds。准备下一次的“可写事件”的select()探測。相同,假设select()发现某句柄捕捉到“可写事件”,则程序应及时做send()操作。并准备好下一次的“可读事件”探測准备。

这样的模型的特征在于每一个运行周期都会探測一次或一组事件,一个特定的事件会触发某个特定的响应。

我们能够将这样的模型归类为“事件驱动模型”。

相比其它模型。使用select() 的事件驱动模型仅仅用单线程(进程)运行。占用资源少,不消耗太多 CPU,同一时候能够为多client提供服务。

假设试图建立一个简单的事件驱动的server程序,这个模型有一定的參考价值。

但这个模型依然有着非常多问题。

首先select()接口并非实现“事件驱动”的最好选择。

由于当须要探測的句柄值较大时,select()接口本身须要消耗大量时间去轮询各个句柄。非常多操作系统提供了更为高效的接口,如linux提供了epoll。BSD提供了kqueue,Solaris提供了/dev/poll。…。

假设须要实现更高效的server程序,相似epoll这样的接口更被推荐。

遗憾的是不同的操作系统特供的epoll接口有非常大差异,所以使用相似于epoll的接口实现具有较好跨平台能力的server会比較困难。

其次,该模型将事件探測和事件响应夹杂在一起。一旦事件响应的运行体庞大,则对整个模型是灾难性的。

单个庞大的运行体1的将直接导致响应其它事件的运行体迟迟得不到运行,并在非常大程度上减少了事件探測的及时性。

幸运的是,有非常多高效的事件驱动库能够屏蔽上述的困难,常见的事件驱动库有libevent库。还有作为libevent替代者的libev库。

这些库会依据操作系统的特点选择最合适的事件探測接口,并且加入了信号(signal) 等技术以支持异步响应,这使得这些库成为构建事件驱动模型的不二选择。

我们将在介绍怎样使用libev库替换select或epoll接口。实现高效稳定的server模型。

事件驱动(event driven I/O)

Libevent 是一种高性能事件循环/事件驱动库。

为了实际处理每一个请求,libevent 库提供一种事件机制。它作为底层网络后端的包装器。事件系统让为连接加入处理函数变得非常简便,同一时候减少了底层IO复杂性。这是 libevent 系统的核心。

创建 libevent server的基本方法是。注冊当发生某一操作(比方接受来自client的连接)时应该运行的函数,然后调用主事件循环 event_dispatch()。运行过程的控制如今由 libevent 系统处理。注冊事件和将调用的函数之后,事件系统開始自治。在应用程序运行时,能够在事件队列中加入(注冊)或 删除(取消注冊)事件。

事件注冊非常方便,能够通过它加入新事件以处理新打开的连接,从而构建灵活的网络处理系统。

使用事件驱动模型实现高效稳定的网络server程序

信号驱动I/O (signal driven I/O (SIGIO))


使用信号驱动I/O时。当网络套接字可读后,内核通过发送SIGIO信号通知应用进程,于是应用能够開始读取数据。有时也称此方式为异步I/O。可是严格讲,该方式并不能算真正的异步I/O。由于实际读取数据到应用进程缓存的工作仍然是由应用自己负责的。

首先我们同意套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不堵塞。当数据准备好时,进程会收到一个SIGIO信号,能够在信号处理函数中调用I/O操作函数处理数据。

Linux下套接字具体解释(三)----几种套接字I/O模型

首先同意套接字使用信号驱动I/O模式。并且通过sigaction系统调用注冊一个SIGIO信号处理程序。当有数据到达后,系统向应用进程交付一个SIGIO信号。然后既能够如图中所看到的那样在信号处理程序中读取套接字数据。然后通知主循环处理逻辑,也能够直接通知主循环逻辑,让主程序进行读取操作。

不管採用上面描写叙述的哪种方式读取数据。应用进程都不会由于尚无数据达到而被堵塞,应用主循环逻辑能够继续运行其它功能。直到收到通知后去读取数据或者处理已经在信号处理程序中读取完毕的数据。

为了让套接字描写叙述符能够工作于信号驱动I/O模式,应用进程必须完毕例如以下三步设置:

1.注冊SIGIO信号处理程序。(安装信号处理器)

2.使用fcntl的F_SETOWN命令,设置套接字全部者。

(设置套接字的全部者)

3.使用fcntl的F_SETFL命令。置O_ASYNC标志,同意套接字信号驱动I/O。(同意这个套接字进行信号输入输出)

注意,必须保证在设置套接字全部者之前,向系统注冊信号处理程序,否则就有可能在fcntl调用后,信号处理程序注冊前内核向应用交付SIGIO信号。导致应用丢失此信号。

以下的程序片段描写叙述了怎样为套接字设置信号驱动I/O:

sigaction 函数:
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact)

该函数会依照參数signum指定的信号编号来设置该信号的处理函数。signum可指定SIGK

ILL和SIGSTOP以外的全部信号,假设參数act不是NULL指针。则用来设置新的信号处理方式。

实际上,Linux内核从2.6開始。也引入了支持异步响应的IO操作,如aio_read, aio_write,这就是异步IO。

异步I/O (asynchronous I/O (the POSIX aio_functions))


Linux下的asynchronous IO其有用得不多,从内核2.6版本号才開始引入。先看一下它的流程:

Linux下套接字具体解释(三)----几种套接字I/O模型

用户进程发起read操作之后,立马就能够開始去做其它的事。而还有一方面,从kernel的角度,当它受到一个asynchronous read之后。首先它会立马返回,所以不会对用户进程产生不论什么block。然后,kernel会等待数据准备完毕。然后将数据复制到用户内存,当这一切都完毕之后,kernel会给用户进程发送一个signal,告诉它read操作完毕了。

使用异步 I/O 大大提高应用程序的性能

异步IO是真正非堵塞的。它不会对请求进程产生不论什么的堵塞。因此对高并发的网络server实现至关重要。

IOCP(I/O Completion Port)

IOCP(I/O Completion Port),常称I/O完毕port。 IOCP模型属于一种通讯模型。适用于(能控制并发运行的)高负载server的一个技术。 通俗一点说。就是用于高效处理非常多非常多的client进行数据交换的一个模型。

或者能够说,就是能异步I/O操作的模型。

Windows下高并发的高性能server通常会採用完毕portIOCP技术,Linux下则会採用Epoll实现一个高性能的I/O.

总结


到眼下为止,已经将四个IO模型都介绍完了。如今回过头来回答最初的那几个问题:blocking和non-blocking的差别在哪,synchronous IO和asynchronous IO的差别在哪。

先回答最简单的这个:blocking与non-blocking。

前面的介绍中事实上已经非常明白的说明了这两者的差别。调用blocking IO会一直block住相应的进程直到操作完毕,而non-blocking IO在kernel还在准备数据的情况下会立马返回。

在说明synchronous IO和asynchronous IO的差别之前,须要先给出两者的定义。Stevens给出的定义(事实上是POSIX的定义)是这样子的:

* A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;

* An asynchronous I/O operation does not cause the requesting process to be blocked;

两者的差别就在于synchronous IO做”IO operation”的时候会将process堵塞。依照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。

有人可能会说,non-blocking IO并没有被block啊。

这里有个非常“狡猾”的地方。定义中所指的”IO operation”是指真实的IO操作,就是样例中的recvfrom这个系统调用。non-blocking IO在运行recvfrom这个系统调用的时候,假设kernel的数据没有准备好,这时候不会block进程。

可是当kernel中数据准备好的时候,recvfrom会将数据从kernel复制到用户内存中,这个时候进程是被block了,在这段时间内进程是被block的。而asynchronous IO则不一样,当进程发起IO操作之后。就直接返回再也不理睬了,直到kernel发送一个信号。告诉进程说IO完毕。在这整个过程中。进程全然没有被block。

还有一种不经常使用的signal driven IO。即信号驱动IO。总的来说,UNP中总结的IO模型有5种之多:堵塞IO,非堵塞IO。IO复用,信号驱动IO,异步IO。前四种都属于同步IO。堵塞IO不必说了。

非堵塞IO ,IO请求时加上O_NONBLOCK一类的标志位。立马返回,IO没有就绪会返回错误,须要请求进程主动轮询不断发IO请求直到返回正确。

IO复用同非堵塞IO本质一样,只是利用了新的select系统调用,由内核来负责本来是请求进程该做的轮询操作。

看似比非堵塞IO还多了一个系统调用开销。只是由于能够支持多路IO,才算提高了效率。信号驱动IO,调用sigaltion系统调用,当内核中IO数据就绪时以SIGIO信号通知请求进程,请求进程再把数据从内核读入到用户空间,这一步是堵塞的。

异步IO,如定义所说,不会由于IO操作堵塞。IO操作全部完毕才通知请求进程。

Linux下的asynchronous IO其有用得不多,从内核2.6版本号才開始引入。先看一下它的流程:

用户进程发起read操作之后。立马就能够開始去做其它的事。

而还有一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立马返回,所以不会对用户进程产生不论什么block。然后,kernel会等待数据准备完毕,然后将数据复制到用户内存,当这一切都完毕之后,kernel会给用户进程发送一个signal。告诉它read操作完毕了。

用异步IO实现的server这里就不举例了。以后有时间另开文章来讲述。异步IO是真正非堵塞的,它不会对请求进程产生不论什么的堵塞,因此对高并发的网络server实现至关重要。

小结


常见网络IO模型有例如以下几类:

堵塞式IO(简单迭代型+多进程或者多线程并发型)

非堵塞式IO

IO复用(select / poll)

信号驱动IO(signal)

异步IO(async)

前四种都是同步。仅仅有最后一种才是异步IO。

Linux下套接字具体解释(三)----几种套接字I/O模型

同步IO引起进程堵塞,直至IO操作完毕。

异步IO不会引起进程堵塞。

IO复用是先通过select调用堵塞。

除了这几个经典的模型之外,还有其它的比方

多进程或者多线程并发I/O

基于事件驱动的server模型和多线程的server模型(Multi-Thread)

windows下的IOCP模型和linux下的epoll模型