五种网络IO模型

五种网络IO模型



概述

网络IO,会涉及两个系统对象:

  1. 内核空间中的内核对象
  2. 用户空间调用IO的进程/线程对象

一次IO read操作,会涉及两个阶段:

  1. 第一阶段:等待网络上的数据分组到达,然后被复制到内核中的某个缓冲区;
  2. 第二阶段:将数据从内核缓冲区拷贝到用户空间中的进程/线程中的缓冲区

因为在以上两个阶段上各有不同的情况,所以出现了五种网络IO模型:

  1. 阻塞IO(blocking IO)
  2. 非阻塞IO(non-blocking IO)
  3. IO多路复用(IO multiplexing)
  4. 信号驱动IO(signal driven IO)
  5. 异步IO(asynchronous IO)

一、阻塞IO(blocking IO)

linux中,默认情况下所有的socket都是blocking的,典型的read操作流程如下:
在这里插入图片描述
当用户进程调用read(),内核就开始了IO的第一个阶段:等待数据就绪。对于网络IO来说,很多时候数据在一开始还没有完全到达(如:还没有收到一个完整的数据包),此时内核就会等待足够的数据的到来。而对于用户进程,这个过程是阻塞的。当内核等待的数据就绪了,会将数据从内核中拷贝到用户内存中,然后返回结果,此时用户进程才解除blocking状态。

所以,blocking IO的特点就是,在网络IO read操作的两个阶段都是阻塞的。

大家接触网络编程都是从listen()/send()/recv()等接口开始的,这些接口大都是阻塞的。使用这些接口,可以很方便地构建Server/Client的模型,下图是一个简单的Server/Client模型:
在这里插入图片描述
实际上,除非特别指定,几乎所有的IO接口(包括socket接口)都是阻塞型的。这给网络编程带来了很大的问题,如在调用send()的同时,线程将被阻塞,在此期间线程不能做其他的运算或相应任何的网络请求。

一个简单的改进方案就是在服务器使用多线程(或多进程)。给每个网络IO都建立一个线程或进程,这样任何一个连接的阻塞都不会影响其他的连接。具体使用哪种,根据情况而定。通常进程的开销远大于线程,所以如果需要同时为较多的Client提供服务,则推荐使用多线程的方式。如果单个服务执行体需要消耗较多的CPU资源,如需要执行大规模或长时间的数据运算或文件访问,这多进程较为安全。

多进程/多线程的模型如下(仍是阻塞模型):
在这里插入图片描述
主线程持续等待客户端的连接请求,如果有连接,则创建新线程,并在新线程中提供服务。
很多人可能不太明白为什么一个sokect可以accept多次。实际上socket的设计者可能特意为多客户端的情况留下了伏笔,让accept()能够返回一个新的socket。下面是accept的接口:

  int accept(int s, struct sockaddr *addr, socklen_t *addrlen)

参数“s”是从socket()、bind()和listen()中创建和沿用下来的fd。执行完bind()和listen()后,操作系统已经开始监听指定IP和端口的所有连接请求,如果有请求,则将该请求接入请求队列。调用accept()接口正是从socket s的请求队列中抽取第一个连接信息,创建一个与“s”同类的新的socket文件描述符fd,新的fd是用于后续的read/recv等操作。如果请求队列中没有请求,accept()将进入阻塞状态。

上述服务端中的两种socket分别为:监听socket和连接socket

多线程/多进程模型似乎解决了为多个客户端提供服务的问题,但并不尽然。如果需要响应成千上万的客户端连接请求,进程/线程的切换、维护会严重消耗系统的资源,大大降低响应效率,不能满足需求。

所以可以尝试使用非阻塞IO来解决这个问题。

二、非阻塞IO(non-blocking IO)

通过设置socket使其变为non-blocking。当对一个non-blocking的socket执行read操作时,流程如下:
在这里插入图片描述
当内核中没有数据就绪时,用户的read操作不会阻塞,会立即返回-1,errno=EAGAIN;当内核中的数据就绪时,收到用户的read操作请求,会立即把数据拷贝到用户空间中,然后返回。所以在非阻塞IO中,用户进程需要不断地主动询问内核数据是否就绪,而这种方式,就会占用大量的CPU时间,不推荐使用,而是通过IO多路复用的方式实现。

其中,在非阻塞状态下,recv返回值说明如下:

  • 大于0:表示接收数据完毕,返回值是接收到的数据长度;
  • 0:连接已正常断开;
  • -1,且errno=EAGAIN:表示数据还未就绪;
  • -1,且errno!=EAGAIN:表示对应的系统错误。

通过fcntl设置为非阻塞IO:

fcntl(fd, F_SETFL, O_NONBLOCK);

三、IO多路复用(IO multiplexing)

IO多路复用,也称为事件驱动IO(event driven IO)。众所周知,select/poll/epoll支持单个进程同时处理多个网络连接IO,其基本原理就是select/poll/epoll会不断轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。流程图如下:
在这里插入图片描述
当用户调用select,整个进程都会阻塞,此时kernel会监视所有select负责的socket,当任何一个socket数据就绪了,select就会返回。此时用户进程再调用read操作,将数据从内核拷贝到用户进程。

注意:如果处理的连接数不是很多,使用select/poll/epoll的web server不一定比使用多线程+阻塞IO的web server性能更好,可能延迟还更大。select/poll/epoll的优势不是对于单个或少量连接能处理得更快,而在于能处理更多的连接。

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,则该fd_set的第16个bit位被标记为1。

select/poll/epoll的区别,待下次再聊。

四、异步IO(asynchronous IO)

Linux下的异步IO用于磁盘IO操作,不用于网络IO,从内核2.6版本才开始引入。流程如下:
在这里插入图片描述
用户发起read操作之后,立即返回。另一方面,kernel受到一个asynchronous read后,先返回,然后等待数据就绪,再将数据拷贝到用户空间,最后再给用户进程发送一个signal,表示read操作完成了。

五、信号驱动IO(signal driven IO)

首先我们允许socket进行型号驱动IO,并安装一个signal处理函数,进程继续运行(不阻塞)。当数据准备好时,进程会受到一个SIGIO信号,可以在sig处理函数中调用IO操作(如read)处理数据。
信号驱动IO模型是应用进程告诉内核:当你的数据报准备好的时候,给我发送一个信号哈,并且调用我的信号处理函数来获取数据报。这个模型是由信号进行驱动。
在这里插入图片描述
这种模型的优势在于等待数据到达期间,进程可以继续执行,不被阻塞。

总结

synchronous IO和 asynchonous IO区别

synchronous IO在做IO操作时,会阻塞。之前说的阻塞IO,非阻塞IO,IO多路复用都是属于同步IO。可能会说,非阻塞IO不是同步的呀。需要注意的是,这里定义的IO操作,是真是的IO操作,就是read()这个系统函数调用。非阻塞IO在执行read()的时候,如果kernel数据还没就绪,此时不会阻塞,但是当数据就绪时,read会将数据从内核空间中拷贝到用户空间,这个过程是阻塞的。
asynchronous IO在做IO操作时,直接返回,不会阻塞,知道内核发送一个信号,告诉进程IO完成了。整个过程是非阻塞的。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值