始于足下.|

MuXinu

园龄:2年7个月粉丝:3关注:1

Linux中的IO模型介绍

一、IO是什么

I/O(Input/Output),中文名为输入/输出,指的是一切操作程序或设备与计算机之间发生的数据传输的过程。它分为IO设备和IO接口两个部分。
  • IO设备,就是指可以与计算机进行数据传输的硬件。最常见的I/O设备有打印机、硬盘、键盘和鼠标。从严格意义上来讲,它们中有一些只能算是输入设备(比如说键盘和鼠标);有一些只是输出设备(如打印机)。
  • IO接口,就是是主机和外设之间的交接界面,通过接口可以实现主机和外设之间的信息交换。
在计算机的世界里,IO的本质就是计算机的核心(CPU和内存)与其它设备之间数据转移的过程。比如数据从磁盘读入到内存,或内存的数据写回到磁盘,都是IO操作。

二、IO如何进行交互?

IO有内存IO、网络IO和磁盘IO三种。通常,我们说的IO指的是后两者。
用户进程中的一个完整IO分为两个阶段:
  • 用户空间与内核空间交互
  • 内核空间与设备空间交互
0
 

三、什么是缓冲区?

应用层的 IO 操作基本都是依赖操作系统提供的 read 和 write 两大系统调用实现。但由于计算机外部设备(磁盘、网络)与内存、CPU 的读写速度相差过大,若直接读写涉及操作系统中断,因此为了减少 OS 频繁中断导致的性能损耗和提高吞吐量,引入了缓冲区的概念。根据内存空间的不同,又可分为内核缓冲区和进程缓冲区。操作系统会对内核缓冲区进行监控,等待缓冲区达到一定数量的时候,再进行 IO 设备的中断处理,集中执行物理设备的实际 IO 操作,通过这种机制来提升系统的性能。至于具体什么时候执行系统中断(包括读中断、写中断)则由操作系统的内核来决定,应用程序不需要关心。

四、什么是同步/异步?什么是阻塞/非阻塞?

同步与异步

  • 同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。(死等结果)
  • 异步,就是当一个异步过程调用发出后,调用者不能立刻得到结果,调用者不用等待这件事完成,可以继续做其他的事情。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。(回调通知)

阻塞与非阻塞

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,CPU不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
  • 非阻塞调用是指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回
同步和异步的概念描述的是用户线程与内核的交互方式。同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式。阻塞是指IO操作需要彻底完成后才返回到用户空间;而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成
 
同步与异步是 两个对象之间的关系,而阻塞与非阻塞是一个对象的状态。

五、Linux中的五种IO模型

阻塞IO模型(blocking I/O)

应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好。 如果数据没有准备好,一直等待….数据准备好了,从内核拷贝到用户空间,IO函数返回成功指示。
  当调用recv()函数时,系统首先查是否有准备好的数据。如果数据没有准备好,那么系统就处于等待状态。当数据准备好后,将数据从系统缓冲区复制到用户空间,然后该函数返回。在套接应用程序中,当调用recv()函数时,未必用户空间就已经存在数据,那么此时recv()函数就会处于等待状态。
0
 

非阻塞IO模型(nonblocking I/O)

我们把一个SOCKET接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中,会大量的占用CPU的时间。上述模型绝不被推荐。
  把SOCKET设置为非阻塞模式,即通知系统内核:在调用Windows Sockets API时,不要让线程睡眠,而应该让函数立即返回。在返回时,该函数返回一个错误代码。如图所示,一个非阻塞模式套接字多次调用recv()函数的过程。前三次调用recv()函数时,内核数据还没有准备好。因此,该函数立即返回WSAEWOULDBLOCK错误代码。第四次调用recv()函数时,数据已经准备好,被复制到应用程序的缓冲区中,recv()函数返回成功指示,应用程序开始处理数据。
0

IO多路复用模型(I/O multiplexing)

简介:主要是select和epoll;对一个IO端口,两次调用,两次返回,比阻塞IO并没有什么优越性;关键是能实现同时对多个IO端口进行监听
  I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数。
  当用户进程调用了select,那么整个进程会被block;而同时,kernel会“监视”所有select负责的socket;当任何一个socket中的数据准备好了,select就会返回。这个时候,用户进程再调用read操作,将数据从kernel拷贝到用户进程。
  这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用(select和recvfrom),而blocking IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。(select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
0
在这种模型中,这时候并不是进程直接发起资源请求的系统调用去请求资源,进程不会被“全程阻塞”,进程是调用select或poll函数。进程不是被阻塞在真正IO上了,而是阻塞在select或者poll上了。Select或者poll帮助用户进程去轮询那些IO操作是否完成。
  不过你可以看到之前都只使用一个系统调用,在IO复用中反而是用了两个系统调用,但是使用IO复用你就可以等待多个描述符也就是通过单进程单线程实现并发处理,同时还可以兼顾处理套接字描述符和其他描述符。

信号驱动IO模型(signal blocking I/O)

允许Socket使用信号驱动 I/O ,还要注册一个 SIGIO 的处理函数,这时的系统调用将会立即返回。然后我们的程序可以继续做其他的事情,当数据就绪时,进程收到系统发送一个 SIGIO 信号,可以在信号处理函数中调用IO操作函数处理数据。
0

异步IO模型(asynchronous I/O)

相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO的两个阶段,进程都是非阻塞的。
Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv。
 
0
 

信号驱动IO和异步IO的区别

信号驱动IO
1.当进程注册信号驱动I/O时,它会告诉内核,当某个文件描述符(如套接字)准备好读或写时,通过发送一个信号(如SIGIO)通知进程。
2.进程接收到信号后,会唤醒并执行一个信号处理函数,此时进程需要重新调用read或write等系统调用来完成实际的数据读写操作。
3.信号驱动I/O的关键点在于内核只负责通知进程可以开始I/O操作,而不会等到整个I/O操作完成。
 
异步I/O
1.异步I/O模型更进一步,当进程发起一个异步I/O请求后(例如使用POSIX的aio_read或aio_write函数),进程可以立即返回而不被阻塞。
2.内核不仅负责在数据准备好时开始I/O操作,还会在I/O操作(包括数据从内核缓冲区复制到用户空间)彻底完成后,通过回调函数或其他形式通知进程。
3.进程在收到内核的通知时,就知道I/O操作已完成,无需再次调用系统调用来完成读写。
 

六、LInux IO模型总结如图所示:

0
 

七、IO多路复用机制

select、poll、epoll都是IO多路复用的机制。IO多路复用就是通过一种机制,让一个进程/线程可以监视多个描述符,一旦某个描述符就绪(一般是读写就绪),能够通知应用程序进行相应的读写操作。
I/O多路复用在英文叫 I/O multiplexing,这里面的 multiplexing 指的其实是在单个进程/线程通过记录跟踪每一个文件描述符的状态来同时管理多个I/O流。发明它的原因,是尽可能地提高服务器的吞吐能力。
I/O复用虽然能同时监听多个文件描述符,当其本质上还是同步IO模型,因为需要在读写事件就绪后程序自己负责进行读写事件的处理,而这个读写过程是阻塞的。如果要实现并发,只能使用多进程/多线程等编程手段了。与多进程/多线程技术相比,I/O多路复用技术最大的优势就是系统开销小,系统不必创建大量进程/线程,也不必维护这些进程/线程,从而大大减少了系统的开销。
 
select:使用 fd_set 结构体来存放被监听的文件描述符的,本质上是使用一个位图结构来存放这些被监听的文件描述符的,因此select能够监听的文件描述符数量是有限制的。同时,fd_set 没有将文件描述符和事件进行绑定,它仅仅是一个文件描述符集合,因此,select需要提供3个fd_set类型的参数来分别传入和传出可读、可写及异常事件。一方面,使得select不能处理更多类型的事件,另一方面,由于内核对fd_set集合的在线修改,使得下次再调用select()函数前不得不重置这3个fd_set集合,这使得编程变成很麻烦,并且容易出错。
poll:使用 struct pollfd结构体来存放被监听的文件描述符,它比select“聪明”的地方就在于它把文件描述符和与其关联的事件都定义在这个结构体中了,从而使得编程接口变得简洁很多,同时内核每次修改的都是pollfd结构体的revents成员,而events成员保持不变,因此下次调用poll()函数时应用程序无须重置pollfd类型的事件集参数。
由于每次select 和 poll 调用都是返回整个用户监听的事件集合(其中包括就绪的和未就绪的),所以应用程序索引就绪文件描述符的时间复杂度为O(n)。
epoll:采用与select 和 poll 完全不同的方式来管理用户注册的事件。它在内核中维护一个事件表,并提供了一个独立的系统调用函数 epoll_ctl来控制往该内核事件表中添加、删除、修改事件。这样,每次调用epoll_wait()函数时,都是直接从内核事件表中取得用户注册的事件,而无须反复从用户空间将这些注册事件读入到内核区中,节省了复制的系统开销。epoll_wait 系统调用中的 events 指针参数仅用来返回就绪的事件,这使得应用程序索引就绪文件描述符的时间复杂度为O(1)。需要注意的是,epoll 和 poll一样,也是将文件描述符和与其关联的事件是绑定在一起的,这样做的好处是,编程接口变得简洁,不像select那样复杂。
 
 
 
0
 
 
 
参考:https://juejin.cn/post/7012061816394088484
 

本文作者:MuXinu

本文链接:https://www.cnblogs.com/MuXinu/p/18090605

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   MuXinu  阅读(31)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起