zoukankan      html  css  js  c++  java
  • Linux IO模型

    简述

    IO操作不外乎读和写,但是不同场景对读写有不同的需求,例如网络中同时监控多个文件句柄,例如关键数据希望一路刷到存储设备而不是扔到cache就返回。

    怎么读,怎么写,等不等结果返回,是否等获取到数据才发返回,组成了不同的IO模型,分别适用于不同的场景。

    根据同步与异步,阻塞与非阻塞,可以把IO模型如下分类:

    同步/异步阻塞/非阻塞怎么理解呢?

    同步与异步,就是IO发起人是否等待数据操作结束。发起一次IO请求后,发起IO的进程死等操作结束才继续往下跑,就是同步。发起一次IO请求后,不等结束直接往下跑,就是异步。

    阻塞与非阻塞,就是没有数据时是否等到有数据才返回。如果没有数据,就让IO进程干等,直到有数据再返回,就是阻塞。如果看到没有数据,直接就返回了,就是非阻塞。

    什么情况下会没有数据呢?一个文件就这么大,除非读到文件末尾了,不然怎么会说没有数据呢?实际上,阻塞与非阻塞并不指磁盘上常规的文件读写,而是指socket或者pipe之类的特殊文件。这些特殊文件并没有明确意义上的大小,就是说,理论上他们的数据是无限大的,只要有人往里面写数据,你就能无限读出数据。这种特殊文件有数据的前提,就是有人往里面”灌“数据。如果你读的太快,别人写得太慢,就会出现池子里面没有数据的情况。这情况并不表示文件读到末端了,只表示暂时还没有数据。这时候你等呢?还是不等呢?这就是阻塞与非阻塞。

    如果是同步/异步是IO发起人是否主动想等待,阻塞/非阻塞就是没有数据时读是否被动等待。

    本文并不会严格按上图依次介绍6种IO模型,相信网上有一大堆的资料。本文尝试从常规读写、多路复用的2个常用场景介绍IO模型。

    上图中的信号IO,需要内核在某个时机向用户空间传递SIGIO信号,且用户空间在捕抓到此信号后进行IO处理。这做法并不常见,本文不会展开介绍。且由于是需要被动通知后才会执行IO操作,因此被我归类为异步IO。

    上图中的事件驱动依赖于某些特定的库。其原理类似于为某些事件注册钩子函数,由库函数实现事件监控。在事件触发时调用钩子函数解决。这些函数库屏蔽了平台之间的差异,例如Linux中通过epoll()来监控多个fd。不同库有不同的使用方法,本文也不会展开介绍。

    常规read()和write()

    读操作

    /* 同步阻塞 */
    fd = open("./test.txt", O_RDONLY); // 常规文件
    ret = read(fd, buf, len);
    
    /* 同步非阻塞 */
    int fds[2];
    ret = pipe2(fds, O_NONBLOCK); // 无名管道
    ret = read(fds[0], buf, len);
    
    /* 同步非阻塞 */
    fd = open("./fifo", O_RDONLY | O_NONBLOCK); // 有名管道
    ret = read(fd, buf, len);
    

    在大多数场景下,我们系统调用read()正确返回时就表示已经读到数据了,此次的IO操作已经结束了。毫无疑问,大多数情况下的读操作,都是同步的。

    是否要阻塞,取决于open()时是否有O_NONBLOCK参数。

    总的来说,我们系统调用read(),除非指定O_NONBLOCK,否则都是同步且阻塞的。

    写操作

    /* 异步 */
    fd = open("./test.txt", O_WRONLY);
    ret = write(fd, buf, len);
    
    /* 同步 */
    fd = open("./test.txt", O_WRONLY | O_SYNC);
    ret = write(fd, buf, len);
    

    阻塞与非阻塞主要针对读数据,写数据主要区分同步还是异步

    由于Linux的IO栈中,有专门为IO准备的Cache层。在正常情况下,写操作只是把数据直接扔到了Cache就返回了,此时数据并没回刷到磁盘。要不等到系统回刷线程主动回刷,要不应用主动调用fsync(),否则数据一直都在Cache层,此时掉电数据就丢了。

    写操作是同步还是异步,主要看open()时,是否带O_SYNC参数。带O_SYNC就是同步写,否则就是异步写。

    本文开头就提到,同步/异步是IO发起人是否主动想等待IO结束,这里的写IO结束,指的是数据完全写入到磁盘,而非write()返回。
    在没有O_SYNC情况下,数据只是写到了Cache,需要内核线程定期回刷,所以此时的write是并没有结束的,因此是异步的。相反,如果有O_SYNC,write()操作会一直等到数据完全写入到磁盘后再返回,所以是同步的。

    从写性能角度来说,异步写会优于同步写。由内核IO调度算法,对写请求进行合并与排序,再一次性写入,效率绝对高于东一块西一块的随机写。因此,除非是担心掉电丢失的关键数据,否则建议使用异步写

    多路复用

    多路复用常用于网络开发,例如每个客户端由一个socket与服务器进行远程通信,此时这个服务程序需要同时监控多个socket,为了避免资源损耗和提高响应速率,就会使用多路复用。

    多路复用是怎么一回事呢?我们假设一下有100个socket,在某一时间可能只有个别socket是有数据的,即客户端向服务端发送的请求数据。此时服务程序怎么监控这100个socket,找出有数据的socket,并做出响应?有一种做法,就是非阻塞读每个socket,没数据直接返回读下一个,有数据(请求)就响应,以此实现轮询。还有一种做法,创建100个进程/线程,每个线程,进程对应一个socket。

    对少量的文件还行,如果文件数量一多,数百个,上千个socket逐一轮询,或者创建上千个线程,这效率得多低啊。可不可以批量等待,当哪怕有一个socket有数据时,内核直接告诉应用那个socket来数据了?可以!这就是内核支持的多路复用的系统调用select()epoll()

    /* select 函数原型 */
    int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    
    /* epoll 函数原型 */
    int epoll_create(int size);
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    

    他们原理大抵相似:

    1. 创建一个文件句柄集合,把要监视的文件句柄按顺序整合到一起
    2. 在有数据时置位对应的标识后返回
    3. 应用通过检查标识就可以知道是哪个socket有数据了,此时读socket即可直接获取数据

    具体的使用方法不在这里详细介绍,网上有总多资料,可以参考《UNIX环境高级编程》。

    本文顺便记录下select()epoll()的优缺点对比:

    select()

    1. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
    2. 每次调用select,都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
    3. select支持的文件描述符数量太小了,默认是1024

    epoll()

    1. epoll不仅仅一个函数,而是切分为3个函数,使得监控新的fd时,不需要拷贝所有的fd集合,只需要拷贝新的fd到内核即可。
    2. epoll采取回调的形式,当某个fd就绪了,就会调用回调,而在回调中,把就绪的fd加入就绪链表
    3. epoll没有数量,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048

    可以发现,epoll()是对select()存在的问题进行针对性的解决。

  • 相关阅读:
    Data Base mysql备份与恢复
    java 乱码问题解决方案
    【知识强化】第二章 物理层 2.1 通信基础
    【知识强化】第二章 进程管理 2.2 处理机调度
    【知识强化】第二章 进程管理 2.1 进程与线程
    【知识强化】第一章 操作系统概述 1.3 操作系统的运行环境
    【知识强化】第一章 网络体系结构 1.1 数据结构的基本概念
    【知识强化】第一章 网络体系结构 1.2 计算机网络体系结构与参考模型
    【知识强化】第一章 网络体系结构 1.1 计算机网络概述
    【知识强化】第一章 操作系统概述 1.1 操作系统的基本概念
  • 原文地址:https://www.cnblogs.com/gmpy/p/12652578.html
Copyright © 2011-2022 走看看