2017-06-28 10:10:46 u011192270 阅读数 377

UNIX下IO模型分析

UNIX下的五种常见IO模型分析,帮助理解

IO操作的两个阶段

以读数据操作为例:
1. 等待内核数据准备(数据拷贝到内核缓冲区)
2. 将数据从内核拷贝到用户空间

IO模型

UNIX下共有五种常见的IO模型:
UNIX下共有五种常见的IO模型

下面以recvfrom接口举例

阻塞IO

默认情况下,所有的套接字都是阻塞的
阻塞IO
调用recvfrom接口,进程在IO操作的两个阶段都会阻塞,直到最终数据拷贝到用户空间或者过程中出现错误才会返回,进程在阻塞状态下是不占用CPU资源的

最常见的错误是发生系统中断,此时需要重读,可参考这里

非阻塞IO

可以通过fcntl(sockfd,F_SETFL,O_NONBLOCK)将套接字设置成非阻塞
非阻塞IO
调用recvfrom接口,无论内核缓冲区是否有可用数据,进程都会立即返回,所以在IO操作的第一阶段是非阻塞的; 若无数据可用,内核将errno设置为为EWOULDBLOCK或者EAGAIN,进程可以使用轮询的方法,保证内核在数据准备好时,能立即拷贝到用户空间; 若有则立即将数据拷贝到用户空间,进程在数据拷贝到用户空间即IO操作的第二阶段是阻塞的;

非阻塞IO过于消耗CPU时间,将大部分时间用于轮询

多路复用IO

多路复用系统调用:select,pollepoll,其中windows平台不支持pollepoll,使用方法可以参考I/O 多路复用之select、poll、epoll详解Linux select/poll和epoll实现机制对比
 多路复用IO
调用select,等待内核数据准备,所以IO操作的第一个阶段,进程是阻塞的,不过是阻塞在多路复用系统调用上,而不是IO系统调用上; 当select返回套接字可读条件时,再调用recvfrom将数据从内核拷贝到用户空间,IO操作的第二阶段,进程是阻塞的

多路复用IO和阻塞IO,在IO操作的两个阶段都是阻塞的,不过多路复用IO使用了两个系统调用,而阻塞IO只使用了一个,所以在连接数不是很多的情况下,阻塞IO可能性能更佳; 多路复用IO的优势在于可以同时监控多个用于IO的文件描述符。

多线程中的阻塞IO,与多路复用IO极为相似

信号驱动IO

信号驱动IO
调用sigaction等系统调用安装信号处理函数,并立即返回,所以IO操作的第一阶段,进程是非阻塞的; 当内核数据准备好时,内核会产生一个信号,通知进程将数据从内核拷贝到用户空间,IO操作的第二阶段,进程是阻塞的

使用方法:IO的多路复用和信号驱动

异步IO

异步IO有一组以aio开头的系统调用,使用方法可参考Linux AIO机制
异步IO
调用异步IO系统调用,给内核传递描述字、缓冲区指针、缓冲区大小(与read相同的三个参数)、文件偏移(与lseek类似),告诉内核当整个操作完成时如何通知我们,并立即返回,在IO操作的两个阶段,进程都不阻塞

总结

5种IO模式比较
- 同步IO和异步IO的主要区别是将数据从内核拷贝到用户空间是否阻塞,前者会在将数据从内核拷贝到用户空间时即IO操作的第二个阶段发生阻塞,而后者则在系统调用后直接返回,直到内核发送信号通知IO操作完成,在IO操作的两个阶段都没有阻塞
- 阻塞IO和非阻塞IO的主要区别是系统调用是否立即返回(默认将数据从内核拷贝到用户空间即IO操作的第二个阶段是立即返回的),前者会在IO操作的两个阶段完成前一直阻塞,后者在内核没有准备好数据的情况下立即返回,即只会在IO操作的第二个阶段阻塞
- 信号驱动IO和异步IO的主要区别在于前者由内核通知我们何时启动一个IO操作,在将数据从内核拷贝到用户空间过程中即IO操作的第一个阶段依旧是阻塞的,而后者是由内核通知我们IO操作何时完成,在IO操作的两个阶段都没有阻塞

知乎上有一个比较生动的例子可以说明这几种模型之间的关系。

Reference

About me

forthebadge
- GitHub:AnSwErYWJ
- Blog:http://www.answerywj.com
- Email:yuanweijie1993@gmail.com
- Weibo:@AnSwEr不是答案
- CSDN:AnSwEr不是答案的专栏

Creative Commons License This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。

2017-10-23 20:49:25 caohao1210 阅读数 1272

IO模型

用一幅图表示所支持的I/O模型

I/O模型

纵向维度是“阻塞(Blocking)”、“非阻塞(Non-blocking)”;横向维度是“同步”、“异步”。总结起来是四种模型 同步阻塞、同步非阻塞;异步阻塞、异步非阻塞 。《Unix网络编程》中划分出了“第五种”模型——“信号驱动式IO”其实属于异步阻塞类型,这种模型的通知方式有多种多样后面展开说明。

同步/异步、阻塞/非阻塞

从内核角度看I/O操作分为两步:用户层API调用;内核层完成系统调用(发起I/O请求)。所以“异步/同步”的是指API调用;“阻塞/非阻塞”是指内核完成I/O调用的模式。用一幅图表示更加明显

同步/异步、阻塞/非阻塞

同步是指函数完成之前会一直等待; 阻塞 是指系统调用的时候进程会被设置为Sleep状态直到等待的事件发生(比如有新的数据)。明白这一点之后再看这五种模型相信就会清晰很多,我们挨个分析:

同步阻塞

这种模型最为常见,用户空间调用API( read 、 write )会转化成一个I/O请求,一直等到I/O请求完成API调用才会完成。这意味着: 在API调用期间用户程序是同步的的;这个API调用会导致系统以阻塞的模式执行I/O,如果此时没有数据则一直“等待”(放弃CPU主动挂起——Sleep状态) (注意,对于硬盘来说是不会出现阻塞的,无论是什么时候读它总是有数据。常见的阻塞设备是终端、网卡之类的)。

同步阻塞

以 read 为例子,它由三个参数组成,第一个函数是文件描述符;第二个是 应用缓冲 ;第三个参数是需要读取的字节数。经过系统调用会以阻塞模式执行I/O,I/O模块读取数据后会放入到PageCache中;最后一步是把数据从PageCache复制到 应用缓冲 。如果I/O请求无法得到满足——没有数据,则主动让出CPU直到 有数据 (注意,即便系统调用让出CPU也未必真的就让出。read函数是同步的,所以CPU还是会被用户空间代码占用)。

同步非阻塞

这种模式通过调用 read 、 write 的时候指定 O_NONBLOCK 参数。和“同步阻塞”模式的区别在于系统调用的时候它是以非阻塞的方式执行,无论是否有数据都会立即返回。 以 read 为例,如果成功读取到数据它返回读取到的字节数;如果此时没有数据则返回-1,同时设置errno为EAGAIN(或者EWOULDBLOCK,二者相同)。所以这种模式下我们一般会用一个“循环”不停的尝试读取数据,处理数据。

异步阻塞

同步模型最主要的问题是占用CPU, 阻塞I/O会主动让出CPU但是用户空间的系统调用还是不会返回依然耗费CPU;非阻塞I/O必须不停的“轮询”不断尝试读取数据(会耗费更多CPU更加低效)。如果仔细分析同步模型霸占CPU的原因不难得出结论——都是在等待数据到来。异步模式正是意识到这一点所以把I/O读取细化为 订阅I/O事件,实际I/O读写,在“订阅I/O事件”事件部分会主动让出CPU直到事件发生 。异步模式下的I/O函数和同步模式下的I/O函数是一样的(都是 read 、 write )唯一的区别是异步模式 “读”必有数据 而同步模式则未必。 常见的异步阻塞函数包括 select , poll , epoll ,这些函数的用法需要花费相当大的篇幅介绍而这篇文章我想集中精力介绍“I/O模型”。以 select 为例我们看一下大致原理

异步阻塞

异步模式下我们的API调用分为两步,第一步是通过 select 订阅读写事件 这个函数会主动让出CPU直到事件发生(设置为Sleep状态,等待事件发生) ;select一旦返回就证明可以开始读了所以第二部是通过 read 读取数据( “读”必有数据 )。

异步阻塞模型之信号驱动

“完美主义者”看了上面的 select 之后会有点不爽——我还要“等待”读写事件(即便 select 会主动让出CPU),能不能有读写事件的时候主动通知我啊?。借助“信号”机制我们可以实现这个,但是这并不完美而且有点弄巧成拙的意思。 具体用法:通过 fcntl 函数设置一个 F_GETFL|O_ASYNC ( 曾经信号驱动I/O也叫“异步I/O”所以才有 O_ASYNC 的说法),当有I/O时间的时候操作系统会触发 SIGIO 信号。在程序里只需要绑定 SIGIO 信号的处理函数就可以了。但是这里有个问题—— 信号处理函数由哪个进程执行呢? ,答案是:“属主”进程。操作系统只负责参数信号而实际的信号处理函数必须由用户空间的进程实现。(这就是设置 F_SETOWN 为当前进程PID的原因) 信号驱动性能要比 select 、 poll 高(避免文件描述符的复制)但是缺点是致命的——*Linux中信号队列是有限制的如果操过这个数字问题就完全无法读取数据。

异步非阻塞

这种模型是最“省事”的模型,系统调用完成之后就只要坐等数据就可以了。是不是特别爽?其实不然,问题出在实现上。Linux上的AIO两个实现版本,POSIX的实现最烂(蓝色巨人的锅)性能很差而且是基于“事件驱动”还会出现“信号队列不足”的问题(所以它就偷偷的创建线程,导致线程也不可控了);一个是Linux自己实现的(redhat贡献)Native AIO。Native AIO主要涉及到的两个函数io_submit 设置需要I/O动作(读、写,数据大小,应用缓冲区等); io_getevents 等待I/O动作完成。没错,即便你的整个I/O行为是非阻塞的还是需要有一个办法知道数据是否读取/写入成功。

异步非阻塞

注意图中,内核不再为I/O分配PageCache,所有的数据必须有用户自己读取到应用缓冲中维护。所以AIO一定是和“直接I/O”配合使用。 AIO针对网卡设备的意义不大,首先它的实现本质上和epoll差不多;其次它在Linux中的作用更多的是用于磁盘I/O(异步非阻塞可以不用多线程就造成大量的I/O请求便于I/O模块“合并”优化会提高整体I/O的吞吐率——而且对CPU开销比较少)。 在Nginx中用了一个技巧,可以实现AIO和epoll联动,AIO读取到数据后触发epoll发送数据。(这个特性是非常尴尬的,如果是磁盘文件完全可以用sendfile搞定)。

Direct I/O和Buffered I/O

Linux在进行I/O操作的时候会先把数据放到PageCache中然后通过“内存映射”的方式返回给应用程序,这样做的好处是可以预读数据也能在多个进程读取相同数据的时候起到Cache的作用。应用程序不能直接使用PageCache中的数据,通常是复制到一块“用户空间”的内存中再使用。
● Direct I/O是指数据不落在PageCache,直接从设备读取到数据后放到用户空间中
● Buffered I/O是指数据竞购PageCache
同步I/O只能使用Buffered I/O;异步阻塞I/O可以Buffered I/O也可以使用Direct I/O;异步非阻塞I/O只能使用Direct I/O

Zero Copy

考虑从磁盘读取文件经过网卡发送出去,会有 四次内存复制 :1. DMA会复制磁盘数据到内核空间,2. 应用程序复制内核空间的数据到用户空间;3. 应用程序用户空间的数据复制到Socket缓冲(内核空间);4. 协议栈把数据复制到网卡的中发送。 简单来说Zero Copy就是 节省这个过程中的内存复制次数 。有几种做法:
● Direct I/O直接把磁盘数据复制到内核空间; 但是Direct I/O没有办法直接把数据放到网卡中——必须要经过协议栈 。所以可以节省一次内存复制;
● sendfile,磁盘数据通过DMA读取到内核空间后直接交给TCP/IP协议栈;真正的不需要内存复制;
除此之外还可以利用 splice 、 mmap 做一些优化,根据不同的设备需要采用不同的方式此处不再展开。


博客学习转载,非原创。

2019-06-18 12:06:10 Daria_ 阅读数 35
一、在网络编程这里需要明确同步/非同步、阻塞/非阻塞
  • 同步:

  所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。

  例如普通B/S模式(同步):提交请求->等待服务器处理->处理完毕返回 这个期间客户端浏览器不能干任何事

  • 异步:

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

  例如 ajax请求(异步): 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕

  • 阻塞

  阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。

  有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。 例如,我们在socket中调用recv函数,如果缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。

  • 非阻塞

  非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
  对象的阻塞模式和阻塞函数调:对象是否处于阻塞模式和函数是不是阻塞调用有很强的相关性,但是并不是一一对应的。阻塞对象上可以有非阻塞的调用方式,我们可以通过一定的API去轮询状 态,在适当的时候调用阻塞函数,就可以避免阻塞。而对于非阻塞对象,调用特殊的函数也可以进入阻塞调用。函数select就是这样的一个例子。

1、用户发起read操作为例,有这样两步操作:

  • 等待数据准备;
  • 将数据从用户空间拷贝到用户空间;
    在这里插入图片描述

同步IO和异步IO的区别就在于:数据拷贝的时候进程是否阻塞;

阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回;

2、对于c/s 模式:

同步:提交请求->等待服务器处理->处理完毕返回这个期间客户端浏览器不能干任何事
异步:请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕

同步和异步都只针对于本机SOCKET而言的。
  同步和异步,阻塞和非阻塞,有些混用,其实它们完全不是一回事,而且它们修饰的对象也不相同。
阻塞和非阻塞是指当进程访问的数据如果尚未就绪,进程是否需要等待,简单说这相当于函数内部的实现区别,也就是未就绪时是直接返回还是等待就绪;

  而同步和异步是指访问数据的机制,同步一般指主动请求并等待I/O操作完毕的方式,当数据就绪后在读写的时候必须阻塞(区别就绪与读写二个阶段,同步的读写必须阻塞),异步则指主动请求数据后便可以继续处理其它任务,随后等待I/O,操作完毕的通知,这可以使进程在数据读写时也不阻塞。

二、Unix提供的五种IO模型:
  1. 同步阻塞IO(blocking I/O):进程会一直阻塞,直到数据拷贝完成

  应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。 如果数据没有准备好,一直等待….数据准备好了,从内核拷贝到用户空间,IO函数返回成功指示。
在这里插入图片描述
  在调用recv()/recvfrom()函数时,发生在内核中等待数据和复制数据的过程。

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

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

  并不是所有Windows Sockets API以阻塞套接字为参数调用都会发生阻塞。例如,以阻塞模式的套接字为参数调用bind()、listen()函数时,函数会立即返回。将可能阻塞套接字的Windows Sockets API调用分为以下四种:

  • 输入操作: recv()、recvfrom()、WSARecv()和WSARecvfrom()函数。以阻塞套接字为参数调用该函数接收数据。如果此时套接字缓冲区内没有数据可读,则调用线程在数据到来前一直睡眠。

  • 输出操作: send()、sendto()、WSASend()和WSASendto()函数。以阻塞套接字为参数调用该函数发送数据。如果套接字缓冲区没有可用空间,线程会一直睡眠,直到有空间。

  • 接受连接:accept()和WSAAcept()函数。以阻塞套接字为参数调用该函数,等待接受对方的连接请求。如果此时没有连接请求,线程就会进入睡眠状态。

  • 外出连接:connect()和WSAConnect()函数。对于TCP连接,客户端以阻塞套接字为参数,调用该函数向服务器发起连接。该函数在收到服务器的应答前,不会返回。这意味着TCP连接总会等待至少到服务器的一次往返时间。

  使用阻塞模式的套接字,开发网络程序比较简单,容易实现。当希望能够立即发送和接收数据,且处理的套接字数量比较少的情况下,使用阻塞模式来开发网络程序比较合适。

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

  1. 同步非阻塞IO(nonblocking I/O)

  非阻塞IO通过进程反复调用IO函数(多次系统调用,并马上返回),在数据拷贝的过程中,进程是阻塞的;
在这里插入图片描述
  我们将一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。

  把SOCKET设置为非阻塞模式,即通知系统内核:在调用Windows Sockets API时,不要让线程睡眠,而应该让函数立即返回。在返回时,该函数返回一个错误代码。图所示,一个非阻塞模式套接字多次调用recv()函数的过程。前三次调用recv()函数时,内核数据还没有准备好。因此,该函数立即返回WSAEWOULDBLOCK错误代码。第四次调用recv()函数时,数据已经准备好,被复制到应用程序的缓冲区中,recv()函数返回成功指示,应用程序开始处理数据。

  当使用socket()函数和WSASocket()函数创建套接字时,默认都是阻塞的。在创建套接字之后,通过调用ioctlsocket()函数,将该套接字设置为非阻塞模式。Linux下的函数是:fcntl().

  套接字设置为非阻塞模式后,在调用Windows Sockets API函数时,调用函数会立即返回。大多数情况下,这些函数调用都会调用“失败”,并返回WSAEWOULDBLOCK错误代码。说明请求的操作在调用期间内没有时间完成。通常,应用程序需要重复调用该函数,直到获得成功返回代码。

  需要说明的是并非所有的Windows Sockets API在非阻塞模式下调用,都会返回WSAEWOULDBLOCK错误。例如,以非阻塞模式的套接字为参数调用bind()函数时,就不会返回该错误代码。当然,在调用WSAStartup()函数时更不会返回该错误代码,因为该函数是应用程序第一调用的函数,当然不会返回这样的错误代码。

  要将套接字设置为非阻塞模式,除了使用ioctlsocket()函数之外,还可以使用WSAAsyncselect()和WSAEventselect()函数。当调用该函数时,套接字会自动地设置为非阻塞方式。

  由于使用非阻塞套接字在调用函数时,会经常返回WSAEWOULDBLOCK错误。所以在任何时候,都应仔细检查返回代码并作好对“失败”的准备。应用程序连续不断地调用这个函数,直到它返回成功指示为止。上面的程序清单中,在While循环体内不断地调用recv()函数,以读入1024个字节的数据。这种做法很浪费系统资源。

  要完成这样的操作,有人使用MSG_PEEK标志调用recv()函数查看缓冲区中是否有数据可读。同样,这种方法也不好。因为该做法对系统造成的开销是很大的,并且应用程序至少要调用recv()函数两次,才能实际地读入数据。较好的做法是,使用套接字的“I/O模型”来判断非阻塞套接字是否可读可写。

  非阻塞模式套接字与阻塞模式套接字相比,不容易使用。使用非阻塞模式套接字,需要编写更多的代码,以便在每个Windows Sockets API函数调用中,对收到的WSAEWOULDBLOCK错误进行处理。因此,非阻塞套接字便显得有些难于使用。

  但是,非阻塞套接字在控制建立的多个连接,在数据的收发量不均,时间不定时,明显具有优势。这种套接字在使用上存在一定难度,但只要排除了这些困难,它在功能上还是非常强大的。通常情况下,可考虑使用套接字的“I/O模型”,它有助于应用程序通过异步方式,同时对一个或多个套接字的通信加以管理。

  1. IO复用(I/O multiplexing)

  主要是select和epoll;对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性;关键是能实现同时对多个IO端口进行监听;
在这里插入图片描述
  I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。

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

  两次调用,两次返回,首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
在这里插入图片描述

  1. 异步非阻塞IO(asynchronous I/O (the POSIX aio_functions))

  数据拷贝的时候进程无需阻塞。
在这里插入图片描述
  当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作

总结:

  • 同步IO引起进程阻塞,直至IO操作完成。
  • 异步IO不会引起进程阻塞。
  • IO复用是先通过select调用阻塞。
三、五种I/O模型的比较

在这里插入图片描述

举个栗子,假如将要举办一场演唱会,小明想要购买这次演唱会的门票

  • 同步阻塞:
    小明从家到售票点买票,得知明天才能买票,小明直接在售票点等待,直到明天买到票之后回家
  • 非阻塞IO:
    小明从家到演唱会现场问售票员买票,但是票还没有出来,然后小明走了,去做其他的事情,过了几个小时再次来询问票有的情况,如果还没出来就继续干其他事,直到票可以买
  • IO复用:
    Java–》selector/Linux–》select、poll、eoll
    小明想去买演唱会的票,打电话告诉黄牛,帮忙留意一下售票时间,出票之后需要小明自己去买票
  • 信号IO:
    小明想买演唱会门票,给举办方打电话,确定售票时间之后通知小明来买票
  • 异步IO:
    小明想买演唱会门票,给举办方打电话,售票之后让快递员把票直接送到小明的家里,小明就不用自己去买票了

1. select、poll、epoll

  epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现

select:

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

  • 单个进程可监视的fd数量被限制,即能监听端口的大小有限。

  一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

  • 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:

  当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

  • 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

poll:

  poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

  • 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义;
  • poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd;

epoll:

  epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知

epoll的优点:

  • 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
  • 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;

  即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

  • 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

2. select、poll、epoll 区别

  • 支持一个进程所能打开的最大连接数

select:

  单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。

poll:

  poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的

epoll:

  虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接;

  • FD剧增后带来的IO效率问题

select、poll:

  因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。

epoll:

  因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

  • 消息传递方式

select、poll:

  内核需要将消息传递到用户空间,都需要内核拷贝动作

epoll:

  epoll通过内核和用户空间共享一块内存来实现的。

  1. 综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。
  • 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调;

  • select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善;

参考文章:https://www.cnblogs.com/renxs/p/3683189.html

2016-06-17 15:40:01 xiaxiaorui2003 阅读数 527

UNIX网络编程中,将IO模型划分为5种:

(1)阻塞IO (2)非阻塞IO (3)信号驱动IO (4)IO复用 (5)异步IO

1、IO操作的2个步骤

以上5种IO操作都分成了两个步骤:发起IO请求实际IO操作

(1)发起IO请求:IO请求一般需要请求特殊资源(如磁盘、RAM、文件),当资源被上一个使用者使用没有被释放时,IO请求就会被阻塞,直到能够使用这个资源。

(2)实际IO操作:真正进行数据接收。

2、IO分类

步骤1(发起IO请求):分为阻塞IO非阻塞IO,区别(看发起IO请求是否阻塞进程):

应用程序调用后,不能立即返回的称为阻塞IO:即资源不可用时,IO请求一直阻塞,直到反馈结果(有数据或超时)。

能立即返回的称为非阻塞IO:资源不可用时,IO请求离开返回,返回数据标识资源不可用。

步骤2(实际IO操作):分为同步IO异步IO,区别(看实际IO操作是否阻塞进程):

在将数据从内核拷贝到用户空间时,将数据拷贝到应用缓冲区期间是否阻塞。

也就是说,如果实际IO读写阻塞请求进程(应用阻塞在发送或接收数据的状态,直到数据成功传输或返回失败),那么就是同步IO。

如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你(应用发送或接收数据后立刻返回,数据写入OS缓存,由OS完成数据发送或接收,并返回成功或失败的信息给应用),那么就是异步IO。

 

综上所述,前4种属于同步IO,最后一种才是异步IO。虽然(2)(3)(4)种IO是非阻塞IO,但其实也只是同步非阻塞,不能算异步。

 

附:UNIX网络编程一书中的截图(来源于网络)

阻塞IO:
非阻塞IO:

 IO复用:

 信号驱动IO:

 异步IO:


5种IO模型比较:

 

2016-05-03 20:12:00 weixin_30414635 阅读数 0

阻塞I/O(bloking I/O)

阻塞IO的特点就是在IO执行的两个阶段(recvfrom和数据从内核空间转移到用户空间)都被block了

非阻塞I/O(non-bloking I/O)


 

非阻塞 IO的特点是用户进程需要不断的主动询问kernel数据是否准备好。

多路复用I/O(multiplexing I/O)

多路复用的特点是通过一种机制一个进程能同时等待IO文件描述符,内核监视这些文件描述符(套接字描述符),其中的任意一个进入读就绪状态,select, poll,epoll函数就可以返回。对于监视的方式,又可以分为 select, poll, epoll三种方式。

异步I/O(asynchronous I/O)

相对于同步I/O,异步I/O不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。I/O两个阶段,进程都是非阻塞的。

同步和异步的区别

通过对上述几种模型的讨论,需要区分阻塞和非阻塞,同步和异步。他们其实是两组概念。区别前一组比较容易,后一种往往容易和前面混合。在我看来,所谓同步就是在整个I/O过程。尤其是拷贝数据的过程是阻塞进程的,并且都是应用进程态去检查内核态。而异步则是整个过程I/O过程用户进程都是非阻塞的,并且当拷贝数据的时是由内核发送通知给用户进程。



 

转载于:https://www.cnblogs.com/vinozly/p/5456400.html

网络IO模型

阅读数 1274

没有更多推荐了,返回首页