什么是 IO(应用程序不能直接操作内核空间需要将数据从内核空间拷贝到用户空间才能使用无论是read操作还是write操作都只能在内核空间里执行) 在计算机操作系统中,所谓的I/O就是 输入(Input)和输出(Output),也可以理解为读(Read)和写(Write),针对不同的对象,I/O模式可以划分为磁盘IO模型和网络IO模型。 IO操作会涉及到用户空间和内核空间的转换,先来理解以下规则:
再来看看所谓的读(Read)和写(Write)操作:
用户空间&内核空间野生程序员对于这个概念可能比较陌生,这其实是 Linux 操作系统中的概念。虚拟内存(操作系统中的概念,和物理内存是对应的)被操作系统划分成两块:User Space(用户空间 和 Kernel Space(内核空间),本质上电脑的物理内存是不划分这些的,只是操作系统开机启动后在逻辑上虚拟划分了地址和空间范围。 操作系统会给每个进程分配一个独立的、连续的虚拟内存地址空间(物理上可能不连续),以32位操作系统为例,该大小一般是4G,即232 。其中将高地址值的内存空间分配给系统内核占用(网上查资料得知:Linux下占1G,Windows下占2G),其余的内存地址空间分配给用户进程使用。 因为我们不是要深入学习操作系统,所以这里以32位系统举例旨在帮助你理解原理。32 位的 LInux 操作系统下,0~3G为用户空间,3~4G为内核空间: 那为什么要这样划分出空间范围呢? 也很好理解,毕竟操作系统身份高贵,太重要了,不能和用户应用程序在一起玩耍,各自的数据都要分开存储并且严格控制权限不能越界。这样才能保证操作系统的稳定运行,用户应用程序太不可控了,不同公司或者个人都可以开发,碰到坑爹的误操作或者恶意破坏系统数据直接宕机玩完了。隔离后应用程序要挂你就挂,操作系统可以正常运行。 简单说,内核空间 是操作系统 内核代码运行的地方,用户空间 是 用户程序代码运行的地方。当应用进程执行系统调用陷入内核代码中执行时就处于内核态,当应用进程在运行用户代码时就处于用户态。 同时内核空间可以执行任意的命令,而用户空间只能执行简单的运算,不能直接调用系统资源和数据。必须通过操作系统提供接口,向系统内核发送指令。 一旦调用系统接口,应用进程就从用户空间切换到内核空间了,因为开始运行内核代码了。 简单看几行代码,分析下是应用程序在用户空间和内核空间之间的切换过程: str = "i am qige" // 用户空间 x = x + 2 file.write(str) // 切换到内核空间 y = x + 4 // 切换回用户空间 上面代码中,第一行和第二行都是简单的赋值运算,在用户空间执行。第三行需要写入文件,就要切换到内核空间,因为用户不能直接写文件,必须通过内核安排。第四行又是赋值运算,就切换回用户空间。 用户态切换到内核态的3种方式:
以上3种方式,除了系统调用是进程主动发起切换,异常和外围设备中断是被动切换的。 查看 CPU 时间在 User Space 与 Kernel Space 之间的分配情况,可以使用top命令。它的第三行输出就是 CPU 时间分配统计。 我们来看看图中圈出来的 CPU 使用率的三个指标: 其中,第一项 7.57% user 就是 CPU 消耗在 User Space 的时间百分比,第二项 7.0% sys是消耗在 Kernel Space 的时间百分比。第三项 85.4% idle 是 CPU 消耗在闲置进程的时间百分比,这个值越低,表示 CPU 越忙。 PIO&DMA大家都知道一般我们的数据是存储在磁盘上的,应用程序想要读写这些数据肯定就需要加载到内存中。接下来给大家介绍下 PIO 和 DMA 这两种 IO 设备和内存之间的数据传输方式。 PIO 工作原理
PIO缺点:每次IO请求都需要CPU多次参与,效率很低。 DMA 工作原理DMA(直接内存访问,Direct Memory Access)。
跟PIO模式相比,DMA就是CPU的一个代理,它负责了一部分的拷贝工作,从而减轻了CPU的负担。 需要注意的是,DMA承担的工作是从磁盘的缓冲区到内核缓冲区或网卡设备到内核的 soket buffer的拷贝工作,以及内核缓冲区到磁盘缓冲区或内核的 soket buffer 到网卡设备的拷贝工作,而内核缓冲区到用户缓冲区之间的拷贝工作仍然由CPU负责。 可以肯定的是,PIO模式的计算机我们现在已经很少见到了。 缓冲IO和直接IO学习用户空间和内核空间的时候我们也说了,用户空间是不能直接访问内核空间的数据的,如果需要访问怎么办?很简单,就需要将数据从内核空间拷贝的用户空间。
缓冲 IO缓冲 IO 也被成为标准 IO,大多数的文件系统系统默认都是以缓冲 IO 的方式来工作的。在Linux的缓冲I/O机制中,数据先从磁盘复制到内核空间的缓冲区,然后从内核空间缓冲区复制到应用程序的地址空间。 接下来我们看看缓冲 IO 下读写操作是如何进行?
操作系统检查内核的缓冲区有没有需要的数据,如果已经缓冲了,那么就直接从缓冲中返回;否则从磁盘中读取到内核缓冲中,然后再复制到用户空间缓冲中。
将数据从用户空间复制到内核空间的缓冲中。这时对用户程序来说写操作就已经完成,至于什么时候再写到磁盘中由操作系统决定,除非显示地调用了sync同步命令。 缓冲I/O的优点:
缓冲I/O的缺点: 在缓冲 I/O 机制中,DMA 方式可以将数据直接从磁盘读到内核空间页缓冲中,或者将数据从内核空间页缓冲直接写回到磁盘上,而不能直接在用户地址空间和磁盘之间进行数据传输,这样数据在传输过程中需要在应用程序地址空间(用户空间)和内核缓冲(内核空间)之间进行多次数据拷贝操作,这些数据拷贝操作所带来的CPU以及内存开销是非常大的。 直接IO顾名思义,直接IO就是应用程序直接访问磁盘数据,而不经过内核缓冲区,也就是绕过内核缓冲区,自己管理I/O缓冲区,这样做的目的是减少一次从内核缓冲区到用户程序缓冲的数据复制。 引入内核缓冲区这个主要是为了提升从磁盘读写数据文件的性能,这也是很多系统优化中常见的手段,多一层缓存可以有效减少很多磁盘 IO 操作;而当用户程序需要向磁盘文件中写入数据时,实际上只需要写入到内核缓冲区便可以返回了,而真正的落盘是有一定的延迟策略的,但这无疑提升了应用程序写入文件的响应速度。 在数据库管理系统这类应用中,它们更倾向于选择自己实现的缓存机制,因为数据库管理系统往往比操作系统更了解数据库中存放的数据,数据库管理系统可以提供一种更加有效的缓存机制来提高数据库中数据的存取性能。 直接I/O的优点: 应用程序直接访问磁盘数据,不经过操作系统内核数据缓冲区,这样做的最直观目的是减少一次从内核缓冲区到用户程序缓冲的数据复制。这种方式通常用在数据库、消息中间件中,由应用程序来实现数据的缓存管理。 直接I/O的缺点: 如果访问的数据不在应用程序缓冲中,那么每次数据都会直接从磁盘进行加载,这种直接加载会非常缓慢。通常 直接I/O 跟 异步I/O 结合使用会得到较好的性能。(异步IO:当访问数据的线程发出请求之后,线程会接着去处理其他事,而不是阻塞等待) IO 访问方式我们常说的 IO 操作,不仅仅是磁盘 IO,还有常见的网络数据传输即网络 IO。 磁盘 IO读操作: 当应用程序调用read()方法时,操作系统检查内核高速缓冲区中是否存在需要的数据,如果存在,那么就直接把内核空间的数据copy到用户空间,供用户的应用程序使用。如果内核缓冲区没有需要的数据,通过通过DMA方式从磁盘中读取数据到内核缓冲区,然后由CPU控制,把内核空间的数据copy到用户空间。 这个过程会涉及到两次缓冲区copy,第一次是从磁盘到内核缓冲区,第二次是从内核缓冲区到用户缓冲区,第一次是DMA的copy,第二次是CPU的copy。 写操作: 当应用程序调用write()方法时,应用程序将数据从用户空间copy到内核空间的缓冲区中(如果用户空间没有相应的数据,则需要从磁盘—>内核缓冲区—>用户缓冲区),这时对用户程序来说写操作就已经完成,至于什么时候把数据再写到磁盘(从内核缓冲区到磁盘的写操作也由DMA控制,不需要cpu参与),由操作系统决定。除非应用程序显示地调用了sync命令,立即把数据写入磁盘。 如果应用程序没准备好写的数据,则必须先从磁盘读取数据才能执行写操作,这时会涉及到四次缓冲区的copy,第一次是从磁盘的缓冲区到内核缓冲区,第二次是从内核缓冲区到用户缓冲区,第三次是从用户缓冲区到内核缓冲区,第四次是从内核缓冲区写回到磁盘。前两次是为了读,后两次是为了写。这其中有两次 CPU 拷贝,两次DMA拷贝。 磁盘IO的延时: 为了读或写,磁头必须能移动到所指定的磁道上,并等待所指定的扇区的开始位置旋转到磁头下,然后再开始读或写数据。磁盘IO的延时分成以下三部分:
网络 IO读操作: 网络 IO 既可以从物理磁盘中读数据,也可以从Socket中读数据(从网卡中获取)。当从物理磁盘中读数据的时候,其流程和磁盘IO的读操作一样。当从Socket中读数据,应用程序需要等待客户端发送数据,如果客户端还没有发送数据,对应的应用程序将会被阻塞,直到客户端发送了数据,该应用程序才会被唤醒,从Socket协议栈(网卡)中读取客户端发送的数据到内核空间(这个过程也由DMA控制),然后把内核空间的数据 copy 到用户空间,供应用程序使用。 写操作: 为了简化描述,我们假设网络IO的数据从磁盘中获取,读写操作的流程如下:
网络IO 的写操作也有四次缓冲区的copy,第一次是从磁盘缓冲区到内核缓冲区(由DMA控制),第二次是内核缓冲区到用户缓冲区(CPU控制),第三次是用户缓冲区到内核缓冲区的 Socket Buffer(由CPU控制),第四次是从内核缓冲区的 Socket Buffer 到网卡设备(由DMA控制)。四次缓冲区的copy工作两次由CPU控制,两次由DMA控制。 网络IO的延时: 网络IO主要延时是由:服务器响应延时+带宽限制+网络延时+跳转路由延时+本地接收延时 决定。一般为几十到几千毫秒,受环境影响较大。所以,一般来说,网络IO延时要大于磁盘IO延时(不过同数据中心的交互除外,会比磁盘 IO 更快)。 零拷贝 IO在上述IO中,一次读写操作要经过四次缓冲区的拷贝,并经历了四次内核态和用户态的切换。 零拷贝(zero copy)IO 技术减少不必要的内核缓冲区跟用户缓冲区之间的拷贝,从而减少CPU的开销和状态切换带来的开销,达到性能的提升。 我们还是对比上面不使用零拷贝时的网络 IO 传输过程来对比分析下: 和上图普通的网络 IO 传输过程对比,零拷贝的传输过程:硬盘 -> kernel buffer (快速拷贝到kernel socket buffer) -> Socket协议栈(网卡设备中)。
这里,只经历了三次缓冲区的拷贝,第一次是从磁盘缓冲区到内核缓冲区,第二次是从内核缓冲区到 kernel socket buffer,第三次是从 kernel socket buffer 到Socket 协议栈(网卡设备中)。只发生两次内核态和用户态的切换,第一次是当应用程序调用read方法时,用户态切换到内核态执行read系统调用,第二次是将数据从网络中发送出去后系统调用返回,从内核态切换到用户态。 零拷贝(zero copy)的应用:
注意:零拷贝要求输入的fd必须是文件句柄,不能是socket,输出的fd必须是socket,也就是说,数据的来源必须是从本地的磁盘,而不能是从网络中,如果数据来源于socket,就不能使用零拷贝功能了。我们看一下sendfile接口就知道了: #include <sys/sendfile.h> ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count)
in_fd 必须指向真实的文件,不能是socket和管道;而out_fd则必须是一个socket。由此可见,sendfile 几乎是专门为在网络上传输文件而设计的。 在Linxu系统中,一切皆文件,因此socket也是一个文件,也有文件句柄(或文件描述符)。 同步&异步、阻塞&非阻塞这两组概念,我接触编程以来,经过听到别人说服务端是 同步非阻塞模型 或者 异步阻塞的 IO 模型,也前后了解过几次,但是理解都不够透彻,特别是这个非阻塞和异步、同步和阻塞的概念很容易懵逼,每个人的说法都不一样,最近我耐心看了几篇文章,这次我感觉我是顿悟了,这里分享下我的理解: 同步和异步是针对应用程序向内核发起任务后的状态而言的:如果发起调用后,在没有得到结果之前,当前调用就不返回,不能接着做后面的事情,一直等待就是同步。异步就是发出调用后,虽然不能立即得到结果,但是可以继续执行后面的事情,等调用结果出来时,会通过状态、通知和回调来通知调用者。 举个例子加深下理解:
阻塞blocking、非阻塞non-blocking,则聚焦的是CPU在等待结果的过程中的状态。 阻塞调用是指调用结果返回之前,当前线程会被挂起,只有在得到结果之后才会返回。你可能会把阻塞调用和同步调用等同起来,实际上它们是不同的,同步只是说必须等到出结果才可以返回,但是等的过程中线程可以是激活的,阻塞是说线程被挂起了。 非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。 比如前面的例子,排队的过程中什么也不能做就是阻塞,CPU 执行权是交出去的;一边排队,一边看手机就是非阻塞,CPU 执行权还在自己手里,但是没看完病之前依旧是在排队死等,所以还是同步的。 总结通过今天的学习,我们掌握了什么是 IO、常见的 IO 操作类型以及对应操作的原理,还有非常重要但是却很容易搞混的同步&异步、阻塞&非阻塞之间的区别,讲解的应该还是比较清楚的。 本文内容还是比较简单的,是一些基础知识,但是如果想深入学习网络编程这些基础是绕不开的,了解了操作系统对于 IO 操作的优化,才能搞明白各种高性能网络服务器的原理。 原文链接: 作者:七哥聊编程 |
|
来自: 山峰云绕 > 《操作系统原理及内核源码文件》