Linux五种I/O模型

《Unix网络编程》中提到的五种I/O模型有

  1. Blocking I/O 阻塞I/O;
  2. NonBlocking I/O 非阻塞I/O;
  3. I/O Multiplexing I/O多路复用;
  4. Signal Driven I/O 信号驱动I/O;
  5. Asynchronous I/O 异步I/O;

前四种属于同步I/O,他们都需要在读写事件就绪后自己负责进行读写,这个读写过程是阻塞的。而异步I/O不需要自己进行读写,可以由硬件进行读写,如采用DMA技术,读写完成后通知进程即可,不需要占用CPU来进行读写。

I/O的两个阶段:

  1. 数据准备(由I/O设备负责);
  2. 数据传输/读写(将数据复制到用户空间)(由CPU或其它硬件负责);

一、阻塞I/O

阻塞I/O指I/O的两个阶段都会被阻塞。
用户进程调用recvfrom系统调用,向内核发起读取数据的请求,此时开始I/O数据准备阶段中,此时,整个进程都会被阻塞。直到数据准备完毕,内核已经成功将数据复制到用户空间后,阻塞才会结束。即进程从调用recvfrom开始直到recvfrom成功返回为止,都处在被阻塞的状态,称为阻塞I/O。阻塞I/O相当于I/O控制方式中的中断驱动方式。

Linux中,默认情况下所有的socket都是阻塞的。

二、非阻塞I/O

当用户进程调用recvfrom向内核请求数据时,如果内核中的数据还没有准备好,内核不会阻塞用户进程,而是立刻返回给进程一个错误信息(EWOULDBLOCK)。用户进程得到这个错误码之后,每隔一段时间再次进行recvfrom操作。一旦内核中的数据准备好了,并且又再次收到了用户进程的system call,它就马上把数据拷贝到用户内存,然后返回。在这种模型中,用户进程会一直占用CPU,不断询问内核,对CPU资源造成了极大浪费,一般不采用这个方法。非阻塞I/O就相当于I/O控制方式中的程序直接控制方式。

Linux中,可以通过设置socket使其变为non-blocking。

三、I/O多路复用

并发的情况下,服务器可能会收到很多读取数据的请求,如果对每个请求都创建一个线程去读取数据,而这些线程由需要采用阻塞或者非阻塞的方式不断调用recvfrom来请求读取数据,这将造成极大的系统开销,浪费了操作系统的资源。

如果可以用一个线程监控多个网络请求,就只需要一个或几个线程就可以完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据。这就是I/O多路复用的思路。在I/O多路复用模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的I/O读写操作。只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程。并且只有在真正有socket读写事件进行时,才会使用I/O资源,大大减少了资源占用。

在非阻塞I/O中,不断地询问socket状态是通过用户线程(ULT)进行的,而在多路复用IO中,轮询每个socket状态是通过内核级线程(KLT)进行的,效率较高。

Linux系统把所有网络请求都以文件描述符(fd)来标识。系统提供了一种函数可以同时监控多个fd,这种函数就是select、poll、epoll函数,有了这个函数后,应用线程通过调用select函数就可以同时监控多个fd,select函数监控的fd中只要有任何一个数据状态准备就绪了,select就会返回可读状态,这时询问线程再去通知处理数据的线程,对应线程此时再发起recvfrom请求去读取数据。

I/O多路复用看起来于阻塞I/O很相似,两个阶段都阻塞。但它与阻塞I/O的一个重要区别就是可以等待多个数据报就绪(因为多用在网络编程中),即可以处理多个连接。select相当于一个代理,调用select以后进程会被select阻塞,这时候在内核空间内select会监听指定的多个socket,如果其中任意一个数据就绪了就返回。此时程序再进行数据读取操作,将数据拷贝至当前进程内。

在select模型中每个socket一般都设置成non-blocking,虽然等待数据阶段仍然是阻塞状态,但是它是被select调用阻塞的,而不是直接被I/O阻塞的。select底层通过轮询机制来判断每个socket读写是否就绪。

select的缺点:底层轮询机制会增加开销、支持的文件描述符数量过少等。Linux引入epoll作为select的改进版本。

四、信号驱动I/O

在信号驱动I/O模型中,进程只在I/O的第二个阶段阻塞,第一个阶段不阻塞,所以一个应用线程也可以同时监控多个fd。

信号驱动I/O不采用循环请求询问的方式去监控数据就绪状态,而是在调用sigaction时候建立一个SIGIO的信号联系,当内核数据准备好之后再通过SIGIO信号通知线程数据准备好后的可读状态,当线程收到可读状态的信号后,再向内核发起recvfrom读取数据的请求。应用线程在发出信号监控后即可返回,不会阻塞,所以在这样的方式下,一个应用线程也可以同时监控多个fd。

首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数,此时请求即刻返回,进程不会阻塞,当数据准备就绪时,就生成对应进程的SIGIO信号,通过信号回调通知应用线程recvfrom来读取数据。

五、异步I/O

异步I/O在网络编程中几乎用不到,在File I/O中可能会用到。

应用进程告知内核启动某个操作,并让内核在整个操作完成之后,通知应用。在这期间进程一直占用CPU,不需要让出CPU给别的进程或者中断处理程序。I/O控制方式中的DMA(存储器直接访问)就是典型的异步I/O,第一阶段由I/O设备负责,第二阶段由DMA控制器负责。CPU只需要做一个预处理和后处理工作。异步I/O需要操作系统的底层支持和硬件支持。

阻塞和非阻塞

发起数据读写请求时,如果数据还没准备就绪,请求已经即可返回,则这时非阻塞。如果进程继续阻塞在这里等待数据,则是阻塞。

同步与异步

同步方法在数据读写时会阻塞进程(I/O的第二阶段),直到I/O操作结束。
异步方法不会阻塞调用者进程,数据复制完成后内核会通知进程数据读写结束。

两种高性能I/O设计模式

两种经典的网络服务设计模式:多线程和线程池
多线程模式
对于每一个新的client,服务器会新建一个线程来处理该client的读写事件。
这种模式对于系统的资源占用非常大。因此,当连接数量达到上限时,再有用户请求连接,就会导致资源瓶颈,严重的可能会直接导致服务器崩溃。

线程池模式
创建一个固定大小的线程池,来一个客户,就从线程池取一个空闲线程来处理,当客户端处理完读写操作之后,就交出对线程的占用。这样就可以避免为每一个客户端都要创建线程带来的资源浪费,使得线程可以重用。

线程池的弊端在于,如果连接大多是长连接,就可能导致在一段时间内,线程池中的线程全部被占用。当再有用户请求连接时,由于没有可用的空闲线程,就会导致客户端连接失败,从而影响用户体验。因此,线程池比较适合大量的短连接应用。

两种高性能模式:Reactor和Proactor
Reactor
会先对每个client注册感兴趣的事件,然后有一个线程专门去轮询每个client是否有事件发生,当有事件发生时,便顺序处理某个事件,当所有事件处理完时,再转去继续轮询。I/O多路复用采用的就是Reactor模式。(为了提高事件处理的速度,可以不用顺序处理而是用多线程或者线程池的方式来处理)

Proactor
当检测到有事件发生时,会新起一个异步事件,然后交由内核线程去处理,当内核线程完成I/O操作之后,发送一个通知告知操作已完成。异步I/O采用的就是Proactor模式。

参考博客一
参考博客二
参考博客三

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值