前提,也是重点是,linux
当接收收据、或者读取数据时,分两步git
1 等待数据准备好。程序员
2 从内核拷贝数据到进程。github
对于一个network IO 即 socket(这里咱们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另外一个就是系统内核(kernel)。当一个read操做发生时,它会经历两个阶段:
1 等待数据准备 (Waiting for the data to be ready)
2 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
记住这两点很重要,由于这些IO Model的区别就是在两个阶段上各有不一样的状况。web
共有5种IO模型。数据库
blocking I/O 阻塞IO
nonblocking I/O 非阻塞IO
I/O multiplexing (select and poll) IO复用
signal driven I/O (SIGIO) 信号驱动IO
asynchronous I/O (the POSIX aio_functions) 异步IO编程
1 blocking IO
在linux中,默认状况下全部的socket都是blocking,一个典型的读操做流程大概是这样:缓存
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来讲,不少时候数据在一开始尚未到达(好比,尚未收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,而后kernel返回结果,用户进程才解除block的状态,从新运行起来。
因此,blocking IO的特色就是在IO执行的两个阶段都被block。tomcat
另外,recvfrom知道数据准备好,且从kernel拷贝到了进程,或者是出错才返回。常见错误时系统调用被信号中断,即recvfrom是慢系统调用。安全
几乎全部的程序员第一次接触到的网络编程都是从 listen()、send()、recv() 等接口开始的。使用这些接口能够很方便的构建服务器 / 客户机的模型。
咱们假设但愿创建一个简单的服务器程序,实现向单个客户机提供相似于“一问一答”的内容服务。
咱们注意到,大部分的 socket 接口都是阻塞型的。所谓阻塞型接口是指系统调用(通常是 IO 接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用得到结果或者超时出错时才返回。
实际上,除非特别指定,几乎全部的 IO 接口 ( 包括 socket 接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用 send() 的同时,线程将被阻塞,在此期间,线程将没法执行任何运算或响应任何的网络请求。这给多客户机、多业务逻辑的网络编程带来了挑战。这时,不少程序员可能会选择多线程的方式来解决这个问题。
一个简单的改进方案是在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每一个链接都拥有独立的线程(或进程),这样任何一个链接的阻塞都不会影响其余的链接。具体使用多进程仍是多线程,并无一个特定的模式。传统意义上,进程的开销要远远大于线程,因此若是须要同时为较多的客户机提供服务,则不推荐使用多进程;若是单个服务执行体须要消耗较多的CPU资源,譬如须要进行大规模或长时间的数据运算或文件访问,则进程较为安全。一般,使用pthread_create ()建立新线程,fork()建立新进程。
咱们假设对上述的服务器 / 客户机模型,提出更高的要求,即让服务器同时为多个客户机提供一问一答的服务。因而有了以下的模型。
图3 多线程的服务器模型
在上述的线程 / 时间图例中,主线程持续等待客户端的链接请求,若是有链接,则建立新线程,并在新线程中提供为前例一样的问答服务。
不少初学者可能不明白为什么一个socket能够accept屡次。实际上socket的设计者可能特地为多客户机的状况留下了伏笔,让accept()可以返回一个新的socket。下面是 accept 接口的原型:
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
输入参数s是从socket(),bind()和listen()中沿用下来的socket句柄值。执行完bind()和listen()后,操做系统已经开始在指定的端口处监听全部的链接请求,若是有请求,则将该链接请求加入请求队列。调用accept()接口正是从 socket s 的请求队列抽取第一个链接信息,建立一个与s同类的新的socket返回句柄。新的socket句柄便是后续read()和recv()的输入参数。若是请求队列当前没有请求,则accept() 将进入阻塞状态直到有请求进入队列。
上述多线程的服务器模型彷佛完美的解决了为多个客户机提供问答服务的要求,但其实并不尽然。若是要同时响应成百上千路的链接请求,则不管多线程仍是多进程都会严重占据系统资源,下降系统对外界响应效率,而线程与进程自己也更容易进入假死状态。
不少程序员可能会考虑使用“线程池”或“链接池”。“线程池”旨在减小建立和销毁线程的频率,其维持必定合理数量的线程,并让空闲的线程从新承担新的执行任务。“链接池”维持链接的缓存池,尽可能重用已有的链接、减小建立和关闭链接的频率。这两种技术均可以很好的下降系统开销,都被普遍应用不少大型系统,如websphere、tomcat和各类数据库等。可是,“线程池”和“链接池”技术也只是在必定程度上缓解了频繁调用IO接口带来的资源占用。并且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。因此使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。
对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“链接池”或许能够缓解部分压力,可是不能解决全部问题。总之,多线程模型能够方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,能够用非阻塞接口来尝试解决这个问题。
2 non-blocking IO
linux下,能够经过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操做时,流程是这个样子:
从图中能够看出,当用户进程发出read操做时,若是kernel中的数据尚未准备好,那么它并不会block用户进程,而是马上返回一个error。从用户进程角度讲 ,它发起一个read操做后,并不须要等待,而是立刻就获得了一个结果。用户进程判断结果是一个error时,它就知道数据尚未准备好,因而它能够再次发送read操做。一旦kernel中的数据准备好了,而且又再次收到了用户进程的system call,那么它立刻就将数据拷贝到了用户内存,而后返回。
因此,用户进程实际上是须要不断的主动询问kernel数据好了没有。就是轮训(Polling),这对cpu是很大的浪费。
因此,non-blocking IO的特色就是在等待数据部阻塞,拷贝阻塞。
以上面临的不少问题,必定程度是 IO 接口的阻塞特性致使的。多线程是一个解决方案,还一个方案就是使用非阻塞的接口。
非阻塞的接口相比于阻塞型接口的显著差别在于,在被调用以后当即返回。使用以下的函数能够将某句柄 fd 设为非阻塞状态。
fcntl( fd, F_SETFL, O_NONBLOCK );
下面将给出只用一个线程,但可以同时从多个链接中检测数据是否送达,而且接受数据。
在非阻塞状态下,recv() 接口在被调用后当即返回,返回值表明了不一样的含义。如在本例中,
能够看到服务器线程能够经过循环调用 recv() 接口,能够在单个线程内实现对全部链接的数据接收工做。
可是上述模型毫不被推荐。由于,循环调用 recv() 将大幅度推高 CPU 占用率;此外,在这个方案中,recv() 更多的是起到检测“操做是否完成”的做用,实际操做系统提供了更为高效的检测“操做是否完成“做用的接口,例如 select()。
3 IO multiplexing
它的基本原理就是select/epoll这个function会不断的轮询所负责的全部socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”全部select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操做,将数据从kernel拷贝到用户进程。这个图和blocking IO的图其实并无太大的不一样,事实上,还更差一些。由于这里须要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。可是,用select的优点在于它能够同时处理多个connection。(多说一句。因此,若是处理的链接数不是很高的话,使用select/epoll的web server不必定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优点并非对于单个链接能处理得更快,而是在于能处理更多的链接。)
在IO multiplexing Model中,实际中,对于每个socket,通常都设置成为non-blocking,可是,如上图所示,整个用户的process实际上是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
因此,IO multiplexing Model的特色就是两个阶段都阻塞,可是等待数据阻塞在select上,拷贝数据阻塞在recfrom上。
在多路复用模型中,对于每个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时间。
下面将从新模拟上例中从多个客户端接收数据的模型。
图7 使用select()的接收数据模型
述模型只是描述了使用select()接口同时从多个客户端接收数据的过程;因为select()接口能够同时对多个句柄进行读状态、写状态和错误状态的探测,因此能够很容易构建为多个客户端提供独立问答服务的服务器系统。以下图。
图8 使用select()接口的基于事件驱动的服务器模型
这里须要指出的是,客户端的一个 connect() 操做,将在服务器端激发一个“可读事件”,因此 select() 也能探测来自客户端的 connect() 行为。
上述模型中,最关键的地方是如何动态维护select()的三个参数readfds、writefds和exceptfds。做为输入参数,readfds应该标记全部的须要探测的“可读事件”的句柄,其中永远包括那个探测 connect() 的那个“母”句柄;同时,writefds 和 exceptfds 应该标记全部须要探测的“可写事件”和“错误事件”的句柄 ( 使用 FD_SET() 标记 )。
做为输出参数,readfds、writefds和exceptfds中的保存了 select() 捕捉到的全部事件的句柄值。程序员须要检查的全部的标记位 ( 使用FD_ISSET()检查 ),以肯定到底哪些句柄发生了事件。
上述模型主要模拟的是“一问一答”的服务流程,因此若是select()发现某句柄捕捉到了“可读事件”,服务器程序应及时作recv()操做,并根据接收到的数据准备好待发送数据,并将对应的句柄值加入writefds,准备下一次的“可写事件”的select()探测。一样,若是select()发现某句柄捕捉到“可写事件”,则程序应及时作send()操做,并准备好下一次的“可读事件”探测准备。下图描述的是上述模型中的一个执行周期。
图9 多路复用模型的一个执行周期
这种模型的特征在于每个执行周期都会探测一次或一组事件,一个特定的事件会触发某个特定的响应。咱们能够将这种模型归类为“事件驱动模型”。
相比其余模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时可以为多客户端提供服务。若是试图创建一个简单的事件驱动的服务器程序,这个模型有必定的参考价值。
但这个模型依旧有着不少问题。首先select()接口并非实现“事件驱动”的最好选择。由于当须要探测的句柄值较大时,select()接口自己须要消耗大量时间去轮询各个句柄。不少操做系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。若是须要实现更高效的服务器程序,相似epoll这样的接口更被推荐。遗憾的是不一样的操做系统特供的epoll接口有很大差别,因此使用相似于epoll的接口实现具备较好跨平台能力的服务器会比较困难。
其次,该模型将事件探测和事件响应夹杂在一块儿,一旦事件响应的执行体庞大,则对整个模型是灾难性的。以下例,庞大的执行体1的将直接致使响应事件2的执行体迟迟得不到执行,并在很大程度上下降了事件探测的及时性。
图10 庞大的执行体对使用select()的事件驱动模型的影响
幸运的是,有不少高效的事件驱动库能够屏蔽上述的困难,常见的事件驱动库有libevent库,还有做为libevent替代者的libev库。这些库会根据操做系统的特色选择最合适的事件探测接口,而且加入了信号(signal) 等技术以支持异步响应,这使得这些库成为构建事件驱动模型的不二选择。下章将介绍如何使用libev库替换select或epoll接口,实现高效稳定的服务器模型。
实际上,Linux内核从2.6开始,也引入了支持异步响应的IO操做,如aio_read, aio_write,这就是异步IO。
4 signal driven I/O (SIGIO)
We can also use signals, telling the kernel to notify us with the SIGIO signal when the
descriptor is ready. We call this signal-driven I/O and show a summary of it in Figure 6.4.
We first enable the socket for signal-driven I/O (as we will describe in Section 25.2) and
install a signal handler using the sigaction system call. The return from this system call is
immediate and our process continues; it is not blocked. When the datagram is ready to be
read, the SIGIO signal is generated for our process. We can either read the datagram from
the signal handler by calling recvfrom and then notify the main loop that the data is ready
to be processed (this is what we will do in Section 25.3), or we can notify the main loop
and let it read the datagram.
Regardless of how we handle the signal, the advantage to this model is that we are not
blocked while waiting for the datagram to arrive. The main loop can continue executing and
just wait to be notified by the signal handler that either the data is ready to process or the
datagram is ready to be read.
因此,signal driven I/O 的特色就是第一个过程没有阻塞,数据准备好的时候会经过SIGIO通知进程,拷贝数据阻塞在recfrom上,优势是进程能够继续执行。
注: signal driven I/O 和 IO multiplexing Model 很像,只是一个阻塞,被动等待,一个会获得通知。
5 Asynchronous I/O
linux下的asynchronous IO其实用得不多。先看一下它的流程:
用户进程发起read操做以后,马上就能够开始去作其它的事。而另外一方面,从kernel的角度,当它受到一个asynchronous read以后,首先它会马上返回,因此不会对用户进程产生任何block。而后,kernel会等待数据准备完成,而后将数据拷贝到用户内存,当这一切都完成以后,kernel会给用户进程发送一个signal,告诉它read操做完成了。
blocking和non-blocking的区别
调用blocking IO会一直block住对应的进程直到操做完成,而non-blocking IO在kernel还准备数据的状况下会马上返回。但二者在从kernel拷贝数据到应用程序的时候都是阻塞的。、
synchronous IO和asynchronous IO的区别
在说明synchronous IO和asynchronous IO的区别以前,须要先给出二者的定义。Stevens给出的定义(实际上是POSIX的定义)是这样子的:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operationcompletes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
二者的区别就在于synchronous IO作”IO operation”的时候会将process阻塞,IO operation 包括两个过程:等待数据+数据拷贝。blocking和noblocking的数据拷贝都要阻塞,按照这个定义,以前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。有人可能会说,non-blocking IO并无被block啊。这里有个很是“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操做,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,若是kernel的数据没有准备好,这时候不会block进程。可是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。而asynchronous IO则不同,当进程发起IO 操做以后,就直接返回不再理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程当中,进程彻底没有被block。
各个IO Model的比较如图所示:
通过上面的介绍,会发现non-blocking IO和asynchronous IO的区别仍是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,可是它仍然要求进程去主动的check,而且当数据准备完成之后,也须要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则彻底不一样。它就像是用户进程将整个IO操做交给了他人(kernel)完成,而后他人作完后发信号通知。在此期间,用户进程不须要去检查IO操做的状态,也不须要主动的去拷贝数据。
参考文献:
IO - 同步,异步,阻塞,非阻塞:http://blog.csdn.net/historyasamirror/article/details/5778378
使用事件驱动模型实现高效稳定的网络服务器程序:http://www.ibm.com/developerworks/cn/linux/l-cn-edntwk/
select函数
1)函数做用:
容许进程指示内核等待多个事件中的一个发生, 并只在有一个或多个事件发生,或经过定时唤醒它。(前面说的同时处理socket描述符和等待用户输入就符合这个状况)(Berkeley的实现容许任何描述符的I/O复用)
2)函数定义
#include <sys/time.h> #include <sys/select.h> int select (int maxfdp1, fd_set *readset, fd_set *writeset, fd_set exceptset, const struct timeval *timeout); struct timeval { long tv_sec; long tv_usec; }
注意select的返回值,是全部文件描述符中准备好的文件描述符的个数。
参数介绍:
timeout:告知内核等待所指定的描述符中任何一个就绪的时间。其有三种可能:
exceptset:目前支持的异常条件只有两个
readset,writeset:咱们要让内核读和写的描述符;
maxfdp1: 指定待测描述符的个数,具体值从0开始到maxfdp1-1(FD_SETSIZE常值为fd_set描述符的总数)。
返回值:如有就绪描述符则为其个数,超时返回0,出错为-1.
select能够做为定时器,此时中间三个描述符集设置为NULL,这个定时器比sleep还有精确,sleep以秒为单位,其以微妙为单位。
3)fd_set类型的相关操做
fd_set rset; //注意新定义变量必定要用FD_ZERO初始化,其自动分配的值不可意料,会致使不可意料的后果。 void FD_ZERO(fd_set *fdset); //initialize the set: all bits off void FD_SET(int fd, fd_set *fdset); //turn on the bit for fd in fdset void FD_CLR(int fd, fd_set *fdset); //turn off the bits for fd in fdset int FD_ISSET(int fd, fd_set *fdset); //is the bit for fd on in fdset
4)套接字准备好的条件
a)该套接字接受缓冲区中的数据字节数大于等于套接字接受缓冲区低水位标记的当前大小。对这样的套接字执行读操做不会阻塞并将返回一个大于0的值(也
就是返回准备好读入的数据)。咱们可使用SO_RCVLOWAT套接字选项设置该套接字的低水位标记。对于tcp和udp套接字而言,其默认值为1。
b)该套接字的读半部关闭(也就是接受了FIN的tcp链接)。对这样的套接字的读操做将不阻塞并返回0.(也就是返回EOF)
c)该套接字是一个监听套接字(就是该套接字掉用过listen,在调用listen函数以后,一个套接字会从主动链接的套接字变身为一个监听套接字,默认是主动套接字)且已完成的链接数不为0。对这样的套接字的accept一般不阻塞。就是上面描述的“select() 也能探测来自客户端的 connect() 行为”(后边可会发文介绍阻塞accept的一种时序条件)
d)其上有一个套接字错误待处理。对这样的套接字的读操做将不阻塞并返回-1(也就是返回一个错误),同时把errno设置成确切的错误条件。这样待处理错误(pending error)也能够经过指定SO_ERROR套接字选项调用getsockopt获取并清除。
a)该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小,而且或者该套接字已链接,或者该套接字不须要链接(如udp套接
字)。这意味着若是咱们把这样的套接字设置成非阻塞,写操做将不阻塞并返回一个正值(例如由传输层接受的字节数)。咱们可使用SO_SNDLOWAT套接字选项来设
置该套接字的低水位标记。对于tcp和udp而言,其默认值一般为2048。
b)该链接的写半部关闭。对这样的套接字的写操做将产生SIGPIPE信号。(链接创建,若某一端关闭链接,而另外一端仍然向它写数据,第一次写数据后会收到对端的RST响应,此后再写数据,内核将向进程发出SIGPIPE信号,通知进程此链接已经断开。而SIGPIPE信号的默认处理是终止程序,)
c) 使用非阻塞connect的套接字已创建链接,或者connect已经以失败了结。(对于阻塞式套接字,调用connect函数将激发TCP的三次握手过程,并且仅在链接创建成功或者出错时才返回;对于非阻塞式套接字,若是调用connect函数会之间返回-1(表示出错),且错误为EINPROGRESS,表示链接创建,创建启动可是还没有完成;若是返回0,则表示链接已经创建,这一般是在服务器和客户在同一台主机上时发生)
d) 其上有一个套接字错误待处理。对这样的套接字的写操做将不阻塞并返回-1(也就是返回一个错误),同时把errno设置成确切的错误条件。这些待处理的错
误也能够经过指定SO_ERROR套接字选项调用getsockopt获取并清除。
注意:当某个套接字上发生错误时,它将select标记为便可读又可写。
select函数的例子,https://github.com/juniperdiego/Unix-network-programming-of-mine/tree/master/tcpserv03
void str_cli(FILE *fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; while (Fgets(sendline, MAXLINE, fp) != NULL) { Writen(sockfd, sendline, strlen(sendline)); if (Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout); } }
对于客户端使用select 代替原来的机制(也就是上述代码),使之可以检测更多的文件描述符。
有三个条件经过套接口处理:
一、若是对方TCP发送数据,套接口就变为可读且read返回大于0的值(即数据的字节数)
二、若是对方TCP发送一个FIN(对方进程终止),套接口就变成为刻度切read返回0(文件结束)
三、若是对方TCP发送一个RST(对方主机崩溃并从新启动),套接口就变为了可读且read返回-1
参看下图
固然,这里只是讲述了select函数的简单实用,没有考虑详细的使用方法,好比,若是在输入文件的时候为批量输入,也就是在输入端保持着一直输入的情况,在最后一个请求发送的时候,还会有接受没有完成应答。输入文件已经结束,可是输入的文件结束符并不意味着咱们已经完成了从套接口的读入,可能仍有请求在去往服务器的路上,或是在去往客户的路上仍有应答。
咱们须要一种方法关闭TCP链接的一半,也就是说,咱们想给服务器发一个FIN,告诉咱们已完成了数据发送,但仍为读而开放套接口描述字。其实这个任务能够由shutdown函数完成。关于这个函数的使用在这里再也不讲述。关闭网络链接的方法为close,可是有的限制能够由shutdown来避免。