后台核心编程(十四):网络编程-网络 I/O 模型


 

IO ( Input/Output ,输入/输出)是计算机体系中重要的一部分。IO 类外设有打印机、键盘、复印机等;存储类型的设备则有硬盘、磁盘、U 盘等;通信设备有网卡、路由器等。不同的IO 设备有着不同的特点:数据率不一样、传送单位不一样、数据表示不一样,等等。所以,很难实现一种统一的输入/输出方法。

IO 有两种操作,同步IO 和异步IO 。同步IO 指的是,必须等待IO 操作完成后,控制权才返回给用户进程。异步IO 指的是,无须等待IO 操作完成,就将控制权返回给用户进程。

网络中的IO ,由于不同的IO 设备有着不同的特点,网络通信中往往需要等待。常见的有以下4 种情况:

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

当一个网络IO (假设是read )发生时,它会涉及两个系统对象, 一个是调用这个IO 的进程,另一个是系统内核。当一个read 操作发生时,它会经历两个阶段:①等待数据准备;②将数据从内核拷贝到进程中。

1 4种网络 I/O 模型

为了解决网络IO 中的问题,学者们提出了4 种网络IO 模型:
①阻塞IO 模型; ②非阻塞IO 模型; ③多路IO 复用模型; ④异步IO 模型。

下面对这4 种模型进行其体讲解:

1.1 阻塞 I/O 模型

在Linux 中,默认情况下所有的soc ket 都是阻塞的, 一个典型的读操作流程如下图所示。

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

当应用进程调用了 recvfrom 这个系统调用后,系统内核就开始了IO 的第一个阶段:准备数据。对于网络IO 来说,很多时候数据在一开始还没到达时(比如还没有收到一个完整的TCP 包),系统内核就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当系统内核一直等到数据准备好了,它就会将数据从系统内核中拷贝到用户内存中,然后系统内核返回结果,用户进程才解除阻塞的状态,重新运行起来。所以,阻塞IO 模型的特点就是在IO 执行的两个阶段(等待数据和拷贝数据)都被阻塞了。

大部分的socket 接口都是阻塞型的。所谓阻塞型接口是指系统调用时(一般是 IO 接口)却不返回调用结果,并让当前线程一直处于阻塞状态只有当该系统调用获得结果或者超时出错时才返回结果。实际上,除非特别指定, 几乎所有的 IO 接口(包括socket 接口)都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用 send() 的同时,线程处于阻塞状态,则在此期间,线程将无法执行任何运算或响应任何网络请求

一个简单的改进方案是在服务器端使用多线程(或多进程) 。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。具体使用多进程还是多线程,并没有一个特定的模式。传统意义上,进程的开销要远远大于线程,所以如果需要同时为较多的客户端提供服务,则不推荐使用多进程;如果单个服务执行体需要消耗较多的CPU 资源,例如需要进行大规模或长时间的数据运算或文件访问,则推荐使用较为安全的进程。通常,使用 pthread_ create () 创建新线程,使用 fork() 创建新进程。(多线程和多进程将在后续章节详细学习)

可以让服务器同时为多个客户端提供一问一答的服务。主线程持续等待客户端的连接请求,如果有连接,则创建新线程,并在新线程中提供与前例相同的问答服务。

很多读者可能不明白为何一个socket 可以accept 多次。实际上socket 的设计者可能特意为多客户端的情况留下了伏笔,让accept()能够返回一个新的socket 。下面是accept 接口的原型:

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

输入参数臼是从 socket()bind()listen() 中沿用下来的 socket 句柄值。执行完 bind()listen() 后,操作系统已经开始在指定的端口处监昕所有的连接请求,如果有请求,则将该连接请求加入请求队列。调用 accept() 接口正是从 socket fd 的请求队列抽取第一个连接信息,创建一个与“同类的新的socket 返回句柄,这个新的socket 句柄即是后续 read()recv() 的输入参数。如果请求队列当前没有请求,则accept() 将进入阻塞状态直到有请求进入队列。

上述多线程的服务器模型似乎完美地解决了为多个客户机提供问答服务的要求,但其实并不尽然。如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应的效率,而线程与进程本身也更容易进入假死状态。

很多程序员可能会考虑使用“线程池”“连接池” 。“线程池”旨在降低创建和销毁线程的频率,使其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”是指维持连接的缓存池,尽量重用已有的连接,降低创建和关闭连接的频率。这两种技术都可以很好地降低系统开销,都被广泛应用于很多大型系统。但是,“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO 接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有“池”的时候效果好多少。所以使用叫池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。

1.2 非阻塞 I/O 模型

在Linux 下,可以通过设置 socket 使IO 变为非阻塞状态。当对一个非阻塞的 socket 执行read 操作时,流程如下图所示:

从上图可以看出,当用户进程发出read 操作时,如果内核中的数据还没有准备好,那么它并不会block 用户进程,而是立刻返回一个错误。从用户进程角度讲,它发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。当用户进程判断结果是一个错误时,它就知道数据还没有准备好,于是它可以再次发送read 操作。一旦内核中的数据准备好了,并且又再次收到了用户进程的系统调用,那么它马上就将数据复制到了用户内存中,然后返回正确的返回值。

所以,在非阻塞式IO 中,用户进程其实需要不断地主动询问kernel 数据是否准备好。非阻塞的接口相比于阻塞型接口的显著差异在于被调用之后立即返回。使用如下的函数可以将某句柄归设为非阻塞状态:

#include <fcntl . h>
fcntl(fd,F_SETFL,O_NONBLOCK);

在非阻塞状态下, recv()接口在被调用后立即返回,返回值代表了不同的含义,如下所述。

  • recv() 返回值大于0 ,表示接收数据完毕,返回值即是接收到的字节数。
  • recv() 返回0 ,表示连接已经正常断开。
  • recv() 返回-1 ,且 ermo 等于 EAGAIN ,表示 recv 操作还没执行完成。
  • recv() 返回-1,且ermo 不等于 EAGAIN ,表示recv 操作遇到系统错误ermo 。

可以看到服务器线程可以通过循环调用recv()接口,可以在单个线程内实现对所有连接的数据接收工作。但是上述模型绝不被推荐,因为循环调用recv()将大幅度占用CPU 使用率; 此外, 在这个方案中 recv()更多的是起到检测“操作是否完成”的作用,实际操作系统提供了更为高效的检测“操作是否完成”作用的接口,例如 select() 多路复用模式,可以一次检测多个连接是存活跃。

1.3 多路10 复用模型

多路IO 复用,有时也称为事件驱动IO 。它的基本原理就是有个函数( select )会不断地轮询所负责的所有socket ,当某个socket 有数据到达了,就通知用户进程, 多路 IO 复用模型的流程如下图所示。

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

这个模型和阻塞 IO 的模型其实并没有太大的不同, 事实上还更差一些。因为这里需要使用两个系统调用( select 和 recvfrom ),而阻塞 IO 只调用了一个系统调用( recvfrom ) 。但是,用select 的优势在于它可以同时处理多个连接。所以,如果处理的连接数不是很高的话,使用 select/epoll 的 Web server 不一定比使用多线程的阻塞 IO 的Web server 性能更好,可能延迟还更大; select /epoll 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

具体的select的用法请看 I/O复用之select

1.4 异步10 模型

用户进程发起read 操作之后,立刻就可以开始去做其他的事;而另一方面,从内核的角度,当它收到一个异步的read 请求操作之后,首先会立刻返回,所以不会对用户进程产生任何阻塞。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存中,当这一切都完成之后,内核会给用户进程发送一个信号,返回read 操作已完成的信息。

调用阻塞IO 会一直阻塞住对应的进程直到操作完成,而非阻塞IO 在内核还在准备数据的情况下会立刻返回。两者的区别就在于同步IO 进行IO 操作时会阻塞进程。按照这个定义,之前所述的阻塞IO 、非阻塞IO 及多路IO 复用都属于同步IO 。实际上,真实的IO操作,就是例子中的recvfrom 这个系统调用。非阻塞IO 在执行recvfrom 这个系统调用的时候,如果内核的数据没有准备好,这时候不会阻塞进程。但是当内核中数据准备好时,recvfrom 会将数据从内核拷贝到用户内存中,这个时候进程则被阻塞。而异步IO 则不一样,当进程发起IO 操作之后,就直接返回,直到内核发送一个信号,告诉进程IO 已完成,则在这整个过程中,进程完全没有被阻塞。

各个I/O模型的比较:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值