什么是IO?在IO模型中,首先讨论什么是IO?在计算机系统中,I/O表示输入(Input)和输出(Output)。操作对象可分为磁盘I/O模型、网络I/O模型、内存映射I/O、DirectI/O、数据库I/O等,只要是具有输入输出类型的交互系统都可以可以认为是I/OSystem,也可以说I/O是整个操作系统进行数据交换和人机交互的通道。这个概念与选择的开发语言无关,是一个笼统的概念。在今天的系统中,I/O有着非常重要的地位。现在系统可能会处理大量的文件和大量的数据库操作,而这些操作都依赖于系统的I/O性能,这就造成了当前系统的瓶颈。两者都是由于I/O性能。因此,为了解决磁盘I/O性能慢的问题,在系统架构中加入了缓存以提高响应速度;或者一些高端服务器从硬件层面入手,采用固态硬盘(SSD)来替代传统的机械硬盘;a系统优化空间往往在低效的I/O环节,很少看到一个系统的CPU和内存的性能是整个系统的瓶颈。那么数据从哪里输入,从哪里输出呢?Input(输入)数据到内存,Output(输出)数据到IO设备(磁盘、网络等需要与内存进行数据交互的设备);主存(通常是DRAM的一块区域)用来缓存文件系统的内容,包括各种数据和元数据。IO接口IO设备与内存之间的直接数据传输是通过IO接口。操作系统封装了IO接口,编程时可以直接使用;对于用户来说,如果要与外设进行通信,只需要使用这些系统调用即可。无处不在的缓存如图所示,当程序调用各种文件操作函数时,用户数据(UserData)到磁盘(Disk)的流向如图所示。图中描述了Linux下文件操作函数的层级关系和内存缓存层的位置。中间的黑色实线是用户态和内核态的分界线。从上到下分析这张图,首先是C语言stdio库定义的相关文件操作函数,是用户态实现的跨平台封装函数。stdio中实现的文件操作函数有自己的stdiobuffer,是用户态实现的缓存。在这里使用缓存的原因很简单——系统调用总是很昂贵。如果用户代码连续读取或写入较小的文件,stdio库通过缓冲区聚合多次读取或写入操作,可以提高程序的效率。stdio库还支持fflush(3)函数主动刷新缓冲区,主动调用底层系统调用立即更新缓冲区中的数据。特别是setbuf(3)函数可以设置stdio库的用户态缓冲区,甚至可以取消缓冲区的使用。在系统调用的read(2)/write(2)和实际的磁盘读写之间也有一层缓冲。在这里,术语内核缓冲区缓存用于指代这一层缓存。在Linux下,文件缓存习惯上称为PageCache,而下层设备的缓存称为BufferCache。这两个概念很容易混淆。这里简单介绍一下概念上的区别:PageCache是??用来缓存文件内容的,跟文件系统关系比较大。文件的内容需要映射到实际的物理磁盘上。这种映射关系由文件系统完成;BufferCache用于缓存存储设备块(如磁盘扇区)的数据,而不管是否有文件系统(BufferCache中缓存的文件系统元数据)。综上所述,既然讨论Linux下的IO操作,自然要跳过stdio库的用户态,直接讨论系统调用级的概念。对stdio库的IO层感兴趣的同学可以自行学习。从上面的描述中还介绍了文件的内核级缓存存放在文件系统的PageCache中。所以下面的讨论基本上讨论了IO相关的系统调用和文件系统PageCache的一些机制。LinuxIO栈虽然我们可以通过系统调用轻松实现外设的数据读取,但实际上这得益于Linux完整的IO栈架构。从图中可以看出,从系统调用接口往下,Linux下的IO栈大致分为三层:文件系统层,以write(2)为例,内核拷贝write()指定的用户态数据2)参数到文件系统Cache中,并及时将块层同步到下层,管理块设备的IO队列,对IO请求进行归并排序(还记得操作系统课上学过的IO调度算法吗?)设备层通过DMAInteraction直接与内存通信,完成数据与具体设备的交互结合这张图,想想Linux系统编程中使用的BufferedIO、mmap(2)、DirectIO。这些机制与LinuxIO堆栈有何关系?上图有点复杂。画个简单的图,加上这些机制的位置:在传统的BufferedIO中使用read(2)读取文件的过程是怎样的?假设要读取一个冷文件(不存在于Cache中),open(2)打开文件内核后创建一系列数据结构,然后调用read(2)到达文件系统层,和发现PageCache中没有文件。这个位置有一个磁盘映射,然后创建一个对应的PageCache,并关联到相关的扇区。然后请求继续到达块设备层,在IO队列中排队,接受一系列调度后到达设备驱动层。此时一般采用DMA方式将对应的磁盘扇区读入Cache,然后read(2)将数据复制到Go到用户提供的用户态缓冲区(read(2的参数指出))).整个过程一共有多少份?如果是第一次从磁盘到PageCache,就是第二次从PageCache到用户态缓冲区。mmap(2)是做什么的?mmap(2)直接将PageCache映射到用户态的地址空间,所以没有mmap(2)方式读取文件的二次复制过程。那么DirectIO做了什么?这个机制比较狠,直接把用户态和blockIO层连接起来,直接抛弃PageCache,直接从磁盘拷贝数据到用户态。有什么好处?写操作直接将进程的缓冲区映射到磁盘扇区,以DMA的形式传输数据,减少了对PageCache层的拷贝,提高了写效率。对于读取来说,第一次肯定比传统方式快,但是后面的读取就不如传统方式了(当然也可以在用户态自己做Cache,一些商业数据库就是这样做)。除了传统的BufferedIO可以以offset+length的形式自由读写文件外,mmap(2)和DirectIO都需要数据按页对齐,而且DirectIO还限制了对底层存储的读写设备块大小的整数倍(甚至Linux2.4也需要文件系统逻辑块的整数倍)。因此,接口越来越低,在明显的效率提升背后,需要在应用层做更多的事情。因此,要想用好这些高级特性,除了深刻理解其背后的机制外,还必须在系统设计上下功夫。阻塞/非阻塞与同步/异步了解了IO的概念之后,现在来解释什么是阻塞、非阻塞、同步、异步。阻塞/非阻塞的对象是调用者自己的情况。阻塞是指调用者调用某个函数后一直在等待函数的返回值,线程处于挂起状态。非阻塞是指调用者调用一个函数后不等待函数的返回值,线程继续运行其他程序(执行其他操作或遍历函数是否有返回值)。同步/异步的对象是被调用者异步是指被调用者被调用后先返回返回值,然后执行函数中包含的功能。其他行为。以下五种IO模型以recvfrom/recv函数为例。这两个函数是操作系统的内核函数,用于从(已连接的)套接字接收数据,并捕获数据传输源的地址。recv函数原型:ssize_trecv(intsockfd,void*buff,size_tnbytes,intflags)sockfd:接收套接字描述符buff:buffernbytes,用于存放recv函数接收到的数据:指定buff的长度flags:一般设置为0本质networkIO是sockets的读取。Sockets在Linux系统中被抽象为流,IO可以理解为对流的操作。对于一次IO访问(以read为例),数据会先被复制到操作系统内核的缓冲区中,然后再从操作系统内核的缓冲区中复制到应用程序的地址空间中。所以,当一个recv操作发生时,会经历两个阶段:第一阶段:等待数据准备好(Waitingforthedatatobeready)。第二阶段:Copyingthedatafromkerneltotheprocess(将数据从内核复制到进程)。对于套接字流:第1步:通常涉及等待数据包到达网络,然后复制到内核中的缓冲区。第二步:将数据从内核缓冲区复制到应用程序进程缓冲区。阻塞IO(BlockingIO)是指调用者调用某个函数后一直在等待函数的返回值,线程处于挂起状态。就像你去商场试衣间,里面有人,你就在门外等着。(完全阻塞)BIO程序流程当用户进程调用recv()/recvfrom()系统调用时,内核开始IO的第一阶段:准备数据(对于网络IO,大部分时候数据还在开头未到达。例如,未收到完整的UDP数据包。此时内核会等待足够的数据到达)。这个过程需要等待,也就是说数据复制到操作系统内核的缓冲区需要一个过程。在用户进程端,整个进程都会被阻塞(当然是进程自己选择阻塞)。第二阶段:当内核等待数据准备好后,会将内核中的数据拷贝到用户内存中,然后内核返回结果,用户进程释放block状态,重新开始运行。所以,阻塞IO的特点就是在IO执行的两个阶段都被阻塞。优点:1、可以及时返回数据,无延迟;2.内核开发者容易;缺点:对于用户来说,在等待的时候要付出性能代价;非阻塞IO是指调用者在调用某个函数后,不等待函数返回值,线程继续运行其他程序(执行其他操作或遍历函数是否有返回值)。就好比你想喝水,但是水还没有烧开,就隔一段时间就去饮水机,直到水烧开。(复制数据时阻塞)非阻塞IO程序流程当用户进程发出读操作时,如果内核中的数据还没有准备好,不会阻塞用户进程,而是立即返回错误。从用户进程的角度来看,它发起读操作后,不需要等待,而是立即得到结果。当用户进程判断结果为错误时,就知道数据没有准备好,可以再次发送读操作。一旦内核中的数据准备好,再次收到用户进程的系统调用,就立即将数据拷贝到用户内存中,然后返回。所以,非阻塞IO的特点就是用户进程需要不断主动询问内核数据是否就绪。与同步阻塞方式相比,同步非阻塞方式的优点是:可以在等待任务完成的同时做其他任务(包括提交其他任务,即多个任务可以同时在“后台”执行时间)。缺点:增加了任务完成的响应延迟,因为读取操作每隔一段时间就会被轮询一次,两次轮询之间的任何时间任务都可能完成。这会导致整体数据吞吐量下降。IO多路复用I/O是指网络I/O,多路复用是指多个TCP连接(即socket或channel),多路复用是指多路复用一个或几个线程。这意味着一个或一组线程处理多个连接。比如上课,学生做完作业举手,老师就下去检查作业。(对于一个IO口,两次调用两次返回和阻塞IO相比没有优势,关键是可以同时监听多个IO口,同时对多个读/写操作执行round-robinIO功能同时进行Inquiry检测,直到有数据可读或可写,才真正调用IO操作函数。)IO多路复用程序流程的模型其实和BIO完全一样,都是阻塞的,只是在socket上加了一层proxyselect,select可以通过监控多个socekts是否有数据来提升性能。一旦检测到一个或多个文件描述有数据到达,select函数返回,然后调用recv函数(这块也是阻塞的),将数据从内核空间复制到用户空间,recv函数返回。多路复用的特点是一个进程可以通过一种机制同时等待IO文件描述符。内核监视这些文件描述符(套接字描述符),其中任何一个进入read-ready状态,select、poll、epoll函数你都可以返回。对于监控方式,可以分为三种方式:select、poll和epoll。IO多路复用在select和epoll等系统调用上被阻塞,但在recvfrom等真正的I/O系统调用上则不会。在I/O编程过程中,当需要同时处理多个客户端访问请求时,可以采用多线程或I/O多路复用技术进行处理。I/O多路复用技术将多个I/O块多路复用到同一个select块中,使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型相比,I/O多路复用的最大优点是系统开销小,系统不需要额外创建新的进程或线程,也不需要维护运行这些进程和线程的组合,降低了成本I/O多路复用的主要应用场景如下:1、服务器需要同时处理多个处于监听状态或多个连接状态的socket。2、服务器端需要同时处理多种网络协议的套接字。用户进程进行系统调用时,在等待数据到来时,处理方式不同,直接等待,轮询,select或者poll轮询,分两阶段处理:第一阶段,有的阻塞,有的不堵,有的能堵,有的不能堵。第二阶段全部阻塞。从整个IO流程来看,它们是顺序执行的,所以可以归类为同步模型(synchronous)。所有进程都主动等待并与内核检查状态。Signal-drivenIO信号驱动IO程序流程在用户态程序中安装SIGIO信号处理函数(使用sigaction函数或signal函数安装自定义信号处理函数),即recv函数。那么用户态程序就可以不被阻塞地进行其他操作了。一旦数据到达,操作系统通过信号的方式通知用户态程序,用户态程序跳转到自定义的信号处理函数。调用信号处理函数中的recv函数接收数据。数据从内核空间复制到用户空间后,recv函数返回。recv函数不会阻塞等待数据到达。这种方式使得异步处理成为可能,而信号是异步处理的基础。在Linux中,通知的方式是信号:如果进程在用户态忙于做其他事情,则强行中断它,并调用预先注册的信号处理函数,它可以决定何时以及如何处理这个异步任务。由于信号处理函数突然闯入,就像中断处理程序一样,有很多事情是做不到的。所以,为了保险起见,一般都是把事件“注册”起来,放到队列中,然后再回到进程原来在干什么。事物。如果进程在内核态忙着做其他事情,比如以同步阻塞的方式读写磁盘,那么它就得暂停通知,等到内核态忙了,即将返回给用户状态,然后触发信号通知。如果现在进程挂起,比如无事休眠,那么唤醒进程,下次CPU空闲的时候,会调度到这个进程,并触发信号通知。异步API说起来容易做起来难,主要针对API实现者。Linux的异步IO(AIO)支持是在2.6.22引入的,很多系统调用都不支持异步IO。Linux的异步IO本来就是为数据库设计的,所以通过异步IO进行的读写操作不会被缓存或缓冲,这样就无法利用操作系统的缓存和缓冲机制。很多人认为linux的O_NONBLOCK是一种异步方式,其实这就是前面说的同步非阻塞方式。需要指出的是,虽然Linux上的IOAPI略显粗糙,但是各个编程框架都有封装好的异步IO实现。操作系统做的工作更少,给用户留下更多的自由。这是UNIX的设计哲学,也是Linux上的编程框架蓬勃发展的原因之一。异步IO异步IO程序流程的效率是最高的。异步IO是通过aio_read函数实现的,aio_read提交请求,在用户态空间提交一个buffer。即使没有数据到达内核,aio_read函数也会立即返回,应用程序可以处理其他事情。当数据到达时,操作系统自动将数据从内核空间复制到aio_read函数提交的用户态缓冲区中。拷贝完成后,通过信号的方式通知用户态程序,用户态程序获取数据后可以进行后续操作。异步IO和信号驱动IO的区别?它位于信号通知用户态程序时数据的位置。异步IO已经将数据从内核空间复制到用户空间;而信号驱动的IO数据还在内核空间,等待recv函数将数据拷贝到用户空间。异步IO主动拷贝数据到用户空间,主动push数据到用户空间,不需要调用recv方法从内核空间拉取数据到用户空间。异步IO是一种推送数据的机制,比通过信号处理IO拉取数据的机制效率更高。直接推送数据,而拉取数据需要调用recv函数,调用该函数会产生额外的开销,效率较低。本文转载自微信公众号“一口Linux”,可通过以下二维码关注。转载本文请联系易口Linux公众号。
