网络IO小白的进阶之路2-阻塞IO

本人这些年看了无数介绍网络IO的技术文章,讲实话,没有一篇完全看懂的,无数次放弃学习,有无数次咬牙坚持学,直到今天看了这篇文章 https://www.cnblogs.com/findumars/p/6361627.html,茅塞顿开,原来网络IO基础并没有很难,所以想在此,和大家分享学习内容,供自己和大家学习。

Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ”,6.2节“I/O Models ”中指出了五种IO模型,分别是:

* blocking IO 阻塞IO
* nonblocking IO 非阻塞IO
* IO multiplexing IO复用
* signal driven IO 信号驱动IO
* asynchronous IO 异步IO

其中,signal driven IO在实际中并不常用,所以主要介绍其余四种IO 模型。
先交代一下IO发生时涉及的对象和步骤。对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,它会经历两个阶段:

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

记住这两点很重要,这两点很重要,这两点很重要(重要的事情说三遍!!!),因为这些IO模型的区别就是在两个阶段上各有不同的情况。

这篇文章主要分享阻塞IO,其他IO模型会再后续文章中分享。

1. 阻塞IO(blocking IO)

在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
在这里插入图片描述

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

listen()、send()、recv() 等接口都是阻塞型的,使用这些接口可以很方便的构建服务器/客户机的模型。

1.1 单线程方式

下面是一个简单地“一问一答”的服务器。
在这里插入图片描述
图 1. 简单的一问一答的服务器 / 客户机模型

所谓阻塞型接口是指系统调用(一般是IO接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回,这给网络编程带来了一个很大的问题,如在调用send()的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求

1.2 多进程/线程 或 线程池 方式

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

我们假设对上述的服务器 / 客户机模型,提出更高的要求,即让服务器同时为多个客户机提供一问一答的服务。于是有了如下的模型。
在这里插入图片描述

图 2. 多线程的服务器模型

在上述的线程 / 时间图例中,主线程持续等待客户端的连接请求,如果有连接,则创建新线程,并在新线程中提供为前例同样的问答服务

为什么一个socket可以accept多次?
实际上socket的设计者可能特意为多客户机的情况留下了伏笔,让accept()能够返回一个新的socket。
下面是 accept 接口的原型:
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
输入参数s是从socket(),bind()和listen()中沿用下来的socket句柄值。
执行完bind()和listen()后,操作系统已经开始在指定的端口处监听所有的连接请求,如果有请求,则将该连接请求加入请求队列。调用accept()接口正是从 socket s 的请求队列抽取第一个连接信息,创建一个与s同类的新的socket返回句柄。新的socket句柄即是后续read()和recv()的输入参数。如果请求队列当前没有请求,则accept() 将进入阻塞状态直到有请求进入队列。

1.2.1 多进程/线程的弊端

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

1.2.2 线程池的弊端

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

总之,阻塞IO不能解决所有问题,但面对大规模的服务请求,可以用非阻塞接口来尝试解决这个问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值