五种网络I/O模型介绍

五种网络I/O模型

阻塞I/O(Blocking I/O)

非阻塞I/O(Non-blocking I/O)

I/O复用(I/O Multiplexing)

信号驱动式I/O(Singnal driven I/O)

异步I/O(Asynchronous I/O)

Tip:前四种都是同步I/O,只有最后一种才是异步I/O。

同步、异步的概念

同步是指一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,本身的任务才能继续执行。

异步是指不需要等待被依赖的任务完成,只需通知被依赖的任务要完成什么工作。然后继续往下执行代码逻辑,只要自己完成了整个任务就算完成了(异步一般使用状态、通知和回调)。

阻塞、非阻塞的概念

阻塞是指调用结果返回之前,当前进程/线程会被挂起,一直处于等待状态,不能够执行其他业务(大部分代码都是这样的)

非阻塞是指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回一个错误(可以继续执行下面代码,或者使用重试机制)

I/O事件发生时涉及的对象和阶段

对于一个网络I/O(这里我们以read举例),它会涉及到两个系统对象,一个是调用这个I/O的用户进程/线程,另一个就是操作系统的系统内核(kernel)。当一个读(read)操作发生时,它会经历两个阶段:

(1) 等待数据准备好 (Waiting for the data to be ready)
(2) 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

【说明】第一步通常涉及到等待数据从网络中到达。当所等待的数据分组到达时,它被存储到内核中的缓冲区中。第二步就是把数据从内核缓冲区复制到用户进程/线程缓冲区。
记住这两点很重要,因为这些I/O Model的区别就是在两个阶段上各有不同的情况。

1、阻塞I/O模型(blocking I/O)

阻塞式I/O模型是最基础的IO模型。在Linux中,默认情况下,所有的socket都是阻塞的。我们以一个UDP数据包套接字的读操作(接收数据)流程为例来分析:

 当用户进程/线程开始调用recvfrom()这个系统调用(system call)时,内核(Kernel)就开始了I/O的第一个阶段:准备数据的到来。对于网络IO来说,很多时候数据在一开始可能还没有到达(比如,还没有收到一个完整的UDP数据包),这个时候内核需要足够的数据到来。当内核收到整个UDP数据报后,内核就认为数据已经准备就绪了,此时接收到的完整的UDP数据报是存放在内核的某个接收缓冲区中。然后,开始读操作的第二个阶段,即将数据从kernel系统缓冲区复制到用户进程/线程的缓冲区中,然后内核返回结果,即recvfrom()系统调用return一个返回值,应用进程/线程开始处理数据报。从调用recvfrom开始到它返回的整段时间内,调用它的进程/线程一直都是处于阻塞状态。只有当recvfrom函数返回了,用户进程/线程才被解除blocking状态,从而可以继续运行起来。

使用socket()函数创建的套接字默认都是阻塞的。所以当调用系统调用函数不能立即完成时,线程处于blocking状态,直到对应的I/O操作完成。但并不是所有系统调用函数以阻塞套接字为参数时,调用都会发生阻塞:

  1. 输入操作:recv()、recvfrom()函数。以阻塞套接字为参数调用该函数接收数据。如果此套接字缓冲区内没有数据可读,则调用线程在数据到来之前一直阻塞。
  2. 输出操作:send()、sendto()函数。以阻塞套接字为参数调用该函数发送数据。如果套接字缓冲区内没有可用空间,线程会一直阻塞,直到有可用空间。
  3. 接受连接:accept()函数。以阻塞套接字为参数调用该函数,等待接受对方的连接请求。如果此时没有连接请求,线程就会进入睡眠状态。
  4. 外出连接:connect()函数。对于TCP连接,客户端以阻塞套接字为参数,调用该函数向服务器发起连接,该函数在收到服务器的应答前,不会返回,线程就会进入睡眠状态,这意味着TCP连接总会等待至少到服务器的一次往返时间。

Blocking IO的特点:在I/O执行的两个阶段,用户进程/线程都是blocking状态。

Blocking IO适用场景:用户希望能够立即发送和接收数据,且处理的socket套接字数量比较少的情况下。

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

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

2、非阻塞I/O模型(non-blocking I/O)

Linux系统下,可以通过设置socket使其变为Non-blocking。当对一个Non-blocking socket执行读操作时,执行流程如下图所示:

从上图可以看出,当用户进程调用recvfrom系统调用时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个EWOULDBLOCK错误。从用户进程角度讲 ,进程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个EWOULDBLOCK错误时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程发起的system call,那么kernel就开始将数据拷贝到了用户进程的缓冲区中,然后返回OK。所以,用户进程其实是需要不断的主动询问kernel数据准备好了没有。

我们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程设置为Waiting态,而是返回一个错误。这样我们的I/O操作函数将不断的循环调用recvfrom函数检查数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止,这个多次我们称之为轮询(polling)。应用进程持续轮询内核,已查看某个IO操作是否就绪,当然这么做会占用大量的CPU时间。

当使用socket()函数创建套接字时,默认是阻塞模式。在成功创建套接字后,在Linux中,可以使用fcntl()函数设置套接字为非阻塞模式的。代码如下:

//创建面向字节流的socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
//设置socket为Non-blocking
flags = fcntl(sockfd, F_GETFL, NULL);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

 当套接字被设置为非阻塞模式后,调用Linux Socket API函数时,调用函数会立即返回。大多数情况下,这些函数调用都会调用“失败”,并返回EWOULDBLOCK错误代码。说明请求的IO操作在调用期间内没有完成。通常,应用程序需要重复调用该函数,直到获得成功返回的代码。需要说明的是并非所有的Socket API函数在非阻塞模式被调用,都返回EWOULDBLOCK错误代码。例如,以非阻塞模式的套接字为参数调用bind()函数时,就不会返回该错误代码。

由于使用非阻塞模式的套接字,其在调用Socket API函数时,会经常返回EWOULDBLOCK错误。所以在任何时候,都应仔细检查返回的代码,并作好应对“失败”的准备,一般是使用if语句检查返回值。应用程序连续不断地调用这个函数,直到它返回成功指示为止,这个需要使用循环语句(如while循环)不断地轮询。

3、I/O复用模型(I/O multiplexing)

在Linux下,可以通过调用select、poll 或者 epoll来实现 I/O复用。

什么叫I/O多路复用?

多路复用的意思是不需要每一个IO事件都由一个单独的进程/线程来操控,只需一个进程/线程就可以操控多个IO事件。这样的进程/线程需要一种预先告知内核的能力,使得内核一旦发现进程/线程指定的一个或多个I/O条件就绪(一般而言是读就绪或者写就绪),内核就通知进程/线程进行相应的读写操作。简单地说,就是指内核一旦发现应用进程指定的一个或者多个I/O条件准备就绪,它就通知该进程。

Linux下I/O多路复用机制的实现方式主要有:select、poll、epoll。I/O多路复用机制,本质上还是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写操作(即将数据从内核空间复制到用户空间),这个读写过程中I/O系统调用是阻塞的,这与异步I/O读写操作是不同的。如下图展示的是select I/O复用模型:

首先,进程阻塞于select函数调用,等待数据报变为可读状态。当select函数返回套接字可读这一条件时,进程调用recvfrom函数,把数据报从内核复制到进程缓冲区。上图所示只是介绍了一个描述符的情况。select的优势在于它可以等待多个描述符就绪,在后续中会详细介绍。

I/O多路复用,也被成为“事件驱动”I/O模型(event-driven I/O),I/O多路复用的实现方式有:select、poll、epoll、kqueue之类的系统调用,在后续博文中会一一详细分析其实现原理。

4、信号驱动式I/O模型(Signal-driven I/O)

信号驱动式I/O模型,就是使用信号,让内核在描述符就绪时发送SIGIO信号通知用户进程,用户进程再通过系统调用读取数据。下图是其概要展示:

 首先,开启套接字的信号驱动式I/O功能,并通过sigaction系统调用注册一个信号处理函数。该系统调用会立刻返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好被读取时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环已准备好待处理数据,也可以立即通知主循环,让它读取数据报。

无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。需要注意的是,将数据从内核复制到用户进程的缓冲区期间,进程是阻塞的。也就说是,在I/O处理的第2阶段,用户进程仍然是处于阻塞状态。

5、异步I/O模型(Asynchronous I/O)

异步I/O(asynchronous I/O)由POSIX规范定义。当设置为异步I/O模型时,进程发起异步系统调用(如aio_read(),aio_write()等),并立即返回。一般而言,这些异步系统调用函数的工作机制是:告知内核启动某个I/O操作,并让内核在整个I/O操作阶段(包括第2阶段中,将数据从内核复制到用户进程的缓冲区)完成后通知用户进程。这种模型与上一种介绍的信号驱动I/O模型的主要区别在于:信号驱动式I/O是由内核通知用户进程何时启动一个I/O操作,而异步I/O模型是由内核通知用户进程I/O操作何时完成。也就是说,驱动式I/O模型是内核告诉用户进程何时开始I/O操作的第2阶段的工作,而异步I/O模型是内核告诉用户进程I/O操作的全部过程是何时结束的。下图是异步I/O模型的流程图:

首先,用户进程调用异步系统调用aio_read()函数(POSIX异步I/O函数以aio_ 或 lio_ 开头),向内核传递文件描述符、缓冲区指针、缓冲区大小(与read函数相同的3个参数)和文件偏移(与lseek类似),并告诉内核当整个I/O操作完成时如何通知用户进程。该系统调用会立刻返回,而且在等待I/O操作完成期间,我们的进程不会被阻塞。上图的例子中,用户进程要求内核在I/O操作完成时产生某个信号,该信号直到数据已复制到应用进程缓冲区才产生,这一点是不同于信号驱动式I/O模型的。

需要注意的是,异步I/O模型看上去很好,因为在整个I/O操作过程中,应用进程都不会被阻塞。但是,在复制内核缓冲区数据到应用进程的缓冲区过程中,是需要CPU参与的,这意味着不受阻的应用进程会和异步系统调用函数争夺CPU的使用权。例如,在http网络连接中,如果并发量比较大,httpd接入的连接数可能就越多,CPU争用情况就越严重,异步函数返回成功信号的速度就越慢。如果不能很好地处理这个问题,异步IO模型也不一定就好。

6、各种I/O模型的比较

 对比上述5种不同的I/O模型。可以看出,前4种I/O模型的主要区别是在第1阶段,而它们的第2阶段是一样的:在数据从内核复制到应用进程的缓冲区期间,进程阻塞于recvfrom系统调用。相反,异步I/O模型在这两个阶段都要处理,从而不同于其他4种I/O模型。

 7、同步I/O和异步I/O对比

POSIX把这两个术语定义如下:

  • 同步I/O操作(synchronous I/O operation) 导致请求进程阻塞,直到I/O操作完成;
  • 异步I/O操作(asynchronous I/O operation) 不导致请求进程阻塞。

根据上述定义,前4种I/O模型:阻塞式I/O模型、非阻塞式I/O模型、I/O复用模型和信号驱动式I/O模型都是同步I/O模型,其中真正的I/O操作(recvfrom)将阻塞进程。只有异步I/O模型与POSIX定义的异步I/O相匹配。

参考

Linux下5种IO模型以及阻塞/非阻塞/同步/异步区别

同步和异步、阻塞和非阻塞,以及五种I/O模型

5种IO模型、阻塞IO和非阻塞IO、同步IO和异步IO

五种IO模型透彻分析

《UNIX网络编程卷1:套接字联网API(第3版)》第6.2章节

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值