linux-io
Foreword
Linux内核在演进过程中,发展出了丰富的IO模型,以应对不同复杂的场景:
阻塞IO(Blocking IO) VS 非阻塞IO(Non-blocking IO):调用端线程在调用后是否立刻返回
同步IO(Sync IO)VS 异步IO(Async IO):在拷贝数据时是否阻塞调用端线程
同步:调用端会一直等待响应,直到返回结果。 异步:调用端发起调用之后立刻返回,不会等待响应。被调用端通过通知机制或者回调函数来通知。 阻塞:被调用端返回结果之前,调用端线程会被挂起,此时线程不可被CPU调度,线程暂停运行。 非阻塞:在被调用端返回前,函数不会阻塞调用端线程,而会立刻返回。
用户态和内核态
Linux系统中分为内核态(Kernel model)和用户态(User model),CPU时间片会在两个model之间切换。
核心态代码拥有完全的底层资源控制权限,可以执行任何CPU指令,访问任何内存地址,其占有的处理机是不允许被抢占的。内核态的指令包括:启动I/O,内存清零,修改程序状态字,设置时钟,允许/终止中断和停机。内核态的程序崩溃会导致PC停机。
用户态是用户程序能够使用的指令,不能直接访问底层硬件和内存地址。用户态运行的程序必须委托系统调用来访问硬件和内存。用户态的指令包括:控制转移,算数运算,取数指令,访管指令(使用户程序从用户态陷入内核态)。
用户态和核心态的切换有三种方式:
系统调用 这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如调用fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。
异常 当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
外围设备的中断 当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
- 保存处理机上下文,包括程序计数器和其他寄存器。
- 更新PCB信息。
- 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
- 选择另一个进程执行,并更新其PCB。
- 更新内存管理的数据结构。
- 恢复处理机上下文。
进程阻塞
正在执行的进程由于一些事情发生,如请求资源失败、等待某种操作完成、新数据尚未达到或者没有新工作做等,由系统自动执行阻塞原语,使进程状态变为阻塞状态。因此,进程阻塞是进程自身的一种主动行为,只有处于运行中的进程才可以将自身转化为阻塞状态。当进程被阻塞,它是不占用CPU资源的。
文件描述符(fd, File Descriptor)
FD用于描述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
缓存I/O
缓存IO又被称作标准IO,大多数文件系统的默认IO 操作都是缓存IO。在Linux的缓存IO 机制中,操作系统会将 IO 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存IO的缺点: 数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
Linux IO模型
IO模型 | 说明 |
---|---|
阻塞I/O | blocking IO |
非阻塞I/O | nonblocking I/O |
I/O 复用 | I/O multiplexing |
信号驱动I/O | signal driven I/O (SIGIO) |
异步I/O | asynchronous I/O |
阻塞IO模型
进程会一直阻塞,直到数据拷贝完成。 应用程序调用一个IO函数,导致应用程序阻塞,等待数据准备好; 数据准备好后,从内核拷贝到用户空间,IO函数返回成功指示。
非阻塞IO模型
通过进程反复调用IO函数,在数据拷贝过程中,进程是阻塞的。
IO复用模型
主要是select和epoll。一个线程可以对多个IO端口进行监听,当socket有读写事件时分发到具体的线程进行处理。
信号驱动IO模型
信号驱动式I/O:首先我们允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
异步IO模型
相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。
五种IO模型比较
阻塞IO和非阻塞IO的区别
调用阻塞IO后进程会一直等待对应的进程完成,而非阻塞IO不会等待对应的进程完成,在kernel还在准备数据的情况下直接返回。
同步IO和异步IO的区别
首先看一下POSIX中对这两个IO的定义:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes; An asynchronous I/O operation does not cause the requesting process to be blocked;
两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。注意到non-blocking IO会一直轮询(polling),这个过程是没有阻塞的,但是recvfrom阶段blocking IO,non-blocking IO和IO multiplexing都是阻塞的。而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。
IO复用
select、poll、epoll。epoll是linux所特有,而select是POSIX所规定,一般操作系统均有实现。
select
select本质是通过设置或检查存放fd标志位的数据结构来进行下一步处理。缺点是:
单个进程可监视的fd数量被限制,即能监听端口的大小有限。一般来说和系统内存有关,具体数目可以cat /proc/sys/fs/file-max察看。32位默认是1024个,64位默认为2048个
对socket进行扫描时是线性扫描,即采用轮询方法,效率低。当套接字比较多的时候,每次select()都要遍历FD_SETSIZE个socket来完成调度,不管socket是否活跃都遍历一遍。会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,就避免了轮询,这正是epoll与kqueue做的
需要维护一个用来存放大量fd的数据结构,会使得用户空间和内核空间在传递该结构时复制开销大
poll
poll本质和select相同,将用户传入的数据拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或主动超时,被唤醒后又要再次遍历fd。它没有最大连接数的限制,原因是它是基于链表来存储的,但缺点是:
大量的fd的数组被整体复制到用户态和内核空间之间,不管有无意义。
poll还有一个特点“水平触发”,如果报告了fd后,没有被处理,那么下次poll时再次报告该fd。
epoll
epoll支持水平触发和边缘触发,最大特点在于边缘触发,只告诉哪些fd刚刚变为就绪态,并且只通知一次。还有一特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一量该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。epoll的优点:
- 没有最大并发连接的限制。
- 效率提升,只有活跃可用的FD才会调用callback函数。
- 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递。
Zero Copy
通过上面的介绍,我们发现,IO的本质就是CPU 将数据从一块存储拷贝到另外一块存储,这个过程中可能会同外部存储交互多次,从而影响到IO的效率。那有没有一些模式,能够避免这个过程呢?答案是Zero Copy。
简单来说,Zero Copy就是减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效地提高数据传输效率;同时零拷贝技术减少了用户应用程序地址空间和操作系统内核地址空间之间因为上下文切换而带来的开销。
综上所述,零拷贝技术的目标可以概括如下:
避免数据拷贝
避免操作系统内核缓冲区之间进行数据拷贝操作。 避免操作系统内核和用户应用程序地址空间这两者之间进行数据拷贝操作。 用户应用程序可以避开操作系统直接访问硬件存储。 数据传输尽量让 DMA 来做。 将多种操作结合在一起
避免不必要的系统调用和上下文切换。
需要拷贝的数据可以先被缓存起来。 对数据进行处理尽量让硬件来做。
直接 I/O拷贝
对于这种数据传输方式来说,应用程序可以直接访问硬件存储,操作系统内核只是辅助数据传输:这类零拷贝技术针对的是操作系统内核并不需要对数据进行直接处理的情况,数据可以在应用程序地址空间的缓冲区和磁盘之间直接进行传输,完全不需要 Linux 操作系统内核提供的页缓存的支持。
页缓存拷贝
在数据传输的过程中,避免数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间进行拷贝。有的时候,应用程序在数据进行传输的过程中不需要对数据进行访问,那么,将数据从 Linux 的页缓存拷贝到用户进程的缓冲区中就可以完全避免,传输的数据在页缓存中就可以得到处理。在某些特殊的情况下,这种零拷贝技术可以获得较好的性能。Linux 中提供类似的系统调用主要有 mmap(),sendfile() 以及 splice()。
写时复制
对数据在 Linux 的页缓存和用户进程的缓冲区之间的传输过程进行优化。该零拷贝技术侧重于灵活地处理数据在用户进程的缓冲区和操作系统的页缓存之间的拷贝操作。这种方法延续了传统的通信方式,但是更加灵活。在Linux中,该方法主要利用了写时复制技术。
前两类方法的目的主要是为了避免应用程序地址空间和操作系统内核地址空间这两者之间的缓冲区拷贝操作。这两类零拷贝技术通常适用在某些特殊的情况下,比如要传送的数据不需要经过操作系统内核的处理或者不需要经过应用程序的处理。第三类方法则继承了传统的应用程序地址空间和操作系统内核地址空间之间数据传输的概念,进而针对数据传输本身进行优化。我们知道,硬件和软件之间的数据传输可以通过使用DMA来进行,DMA进行数据传输的过程中几乎不需要CPU参与,这样就可以把CPU解放出来去做更多其他的事情,但是当数据需要在用户地址空间的缓冲区和Linux操作系统内核的页缓存之间进行传输的时候,并没有类似DMA这种工具可以使用,CPU需要全程参与到这种数据拷贝操作中,所以这第三类方法的目的是可以有效地改善数据在用户地址空间和操作系统内核地址空间之间传递的效率。
DPDK
DPDK 是一个开源的数据平面开发工具集,提供了一个用户空间下的高效数据包处理库 函数,它通过环境抽象层旁路内核协议栈、轮询模式的报文无中断收发、优化内存/缓冲区/ 队列管理、基于网卡多队列和流识别的负载均衡等多项技术,实现了在 x86 处理器架构下的 高性能报文转发能力,用户可以在 Linux 用户态空间开发各类高速转发应用,也适合与各类 商业化的数据平面加速解决方案进行集成。
Comments powered by Disqus.