网络IO模型

网络IO模型

IO是计算机体系中重要的一部分。IO有两种操作,同步IO和异步IO。同步IO是指必须等待IO操作完成后控制权才能返回给用户进程。异步IO指的是,无需等待IO操作完成,就将控制权返回给用户进程。

网络中的IO常见如下情况:

  1. 输入操作:等待数据到达套接字接受缓冲区
  2. 输出操作:等待套接字发送缓存区有足够的空间容纳将要发送的数据
  3. 服务器接受连接请求:等待新的用户请求到来
  4. 客户端发送连接请求:等待服务器送回客户SYN对应的ACK

4种IO模型

1. 阻塞IO模型

在Linux中,默认所有的socket都是阻塞的。

阻塞IO模型

阻塞和非阻塞的概念描述的是用户线程调用内核IO的操作方式:阻塞是指IO操作彻底完成后才返回到用户空间,非阻塞是指IO调用后立即返回给用户一个状态值。

当进程调用了recvform这个系统调用后,系统内核开始了IO的第一阶段,准备数据。对于网络IO来说,很多数据在一开始没到达时(还没收到一个完整的TCP包) 系统内核等待数据的到来。而用户进程整个会被阻塞。阻塞IO模型的特点就是在 IO执行的两个阶段(等待数据、拷贝数据)被阻塞。

大部分socket接口都是阻塞型的。所谓阻塞型是指系统调用不返回结果,让当前线程一直阻塞。这给网络编程带来了一个重要的问题,如果调用send()时,线程处于阻塞状态,无法响应任何网络请求。

一个简单的改进方案是在服务器端使用多线程。多线程目的是让每个链接都拥有独立的线程。这样一个阻塞的连接不会影响其他连接。传统意义上进程开销远大于线程,所以客户端较多时多线程,单个服务占用资源较多选择安全的多进程。pthread_create()创建新线程,fork() 创建新进程

在socket设计之初,一个句柄就可以被accept()多次。

1
int accept(int fd, struct sockaddr *addr, socklen_t *addrlen)

调用accept正是从 请求队里中抽出第一个连接信息,创建一个与fd同类的新socket返回句柄。如果当前没有请求,accept便会阻塞至有新的请求进入。

上述多线程的服务器模型似乎完美解决了多个客户机应答的需求,但是并不是这样。这主要是因为相应成千上百路的需求对于多进程、多线程都会严重占用系统资源。即使是考虑到线程池和连接池。

线程池旨在降低创建和销毁线程的频率,维护一定数量的线程,并让空闲的线程重新承担新的执行任务。

连接池位置连接的缓存池,尽量重用已有的连接,降低创建和关闭连接 的频率。

这样两种方法广泛应用于大型系统,然而池终有上限,当请求大大超过上限时,池效果并不好。现实中面临上千、上万次的用户请求,多次按成模型会遇到瓶颈。

2. 非阻塞IO模型

Linux下可以设置socket使其为非阻塞状态。

1547041474708

如图所示,当用户发出read操作时,如果内核数据还没准备好,它不会block用户进程而是返回一个错误。从用户进程角度讲,read后不需要等待而是得到一个结果。当用户进程判断其为错误时,就知道它还没准备好,这样便可以再次read操作。

所以非阻塞IO中,用户需要不断询问kernel是否准备好数据。

1
fcnt1(fd, F_SETFL, O_NONBLOCK);

在非阻塞状态下recv()被调用后立即返回。

recv()大于0表示数据接收完毕,返回接收字节数。0表示断开;-1表示没完成或系统错误。

实际操作系统提供了更高效的接口,例如 select() 多路复用

3. 多路IO复用模型

多路IO复用有时也被称为事件驱动IO,它的基本原理是有一个函数(select)不断的轮询所有的(socket),当某个socket数据到达了,就通知用户进程。

1547123108986

当用户调用了select,那么整个进程会被阻塞,而同时内核会监视所有的socket,当某一个socket数据准备好了,select就会返回。这个时候用户进程再进行read操作将数据从内核拷贝到用户进程。

这个模型其实和阻塞IO没有太大的区别,事实上还更冗余,因为需要调用两个系统调用(select、recvfrom)而阻塞IO只需要调用一个 recvfrom 即可。但是select优势在于它可以处理多个连接。多以如果连接数并不高的情况下,select、epoll 不一定比阻塞IO的性能更好。

多路IO复用中,每一个socket一般都设置为非阻塞,而整个用户进程其实是被阻塞的,只不过进程是被select这个进程阻塞,而不是被socket IO阻塞。

1547123933843

这种模型的特征在于每个周期探测一次或一组事件,一个特定的时间会出发某个特定的响应,也被称为“事件驱动模型”。相比其他模型,select() 事件只用单线程执行、占用资源少,不消耗太多CPU资源,同时能为多客户端提供服务。

但是这个模型有很多问题,首先select() 接口本身需要消耗时间去轮询句柄,很多操作系统提供了更方便的接口 Linux 是 epoll BSD提供了 kqueue

4. . 异步IO模型

1547124238726

当用户发起read操作后,立刻就去其他工作;另一个方面,内核收到一个请求后会立刻返回。然后内核等待数据准备完成,然后拷贝到用户内存中,再向用户进程发送一个信号。

调用阻塞IO会一直阻塞IO直到操作完成,而非阻塞IO在内核准备数据的情况下就会立刻返回。二者的区别在于同步IO进行IO时会阻塞进程。按照这个定义,阻塞IO、非阻塞IO、多路IO复用都属于同步IO。

1547124446666

select

select函数是socket编程中一个重要的函数,可以完成非阻塞工作程序,可以监视文件描述符的变化情况。

1
int select(int maxfdp, fdp_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval* timeout);

这里涉及到两个结构体 fd_settimeval 这个里fd_set理解为一个集合,存放文件描述符,及文件句柄。当然UNINX下任何设备、管道、FIFO都是文件形式,所以socket 就是一个文件。