• 浅谈 Linux IO


    公众号关注 「开源Linux」

    回复「学习」,有我为您特别筛选的学习资料~

    来源于:360云计算

    1

    前言

    Linux IO是文件存储的基础。本文参考了网上博主的一些文章,主要总结了LinuxIO的基础知识。

    2

    linux IO栈

    Linux文件IO采用分层的设计。分层有两个好处:

    1. 架构清晰;

    2. 功能解耦;

    Linux文件IO的时候,从通用性和性能的角度考虑,采用了一个折中的方案来满足我们日常写磁盘。

    例如:

    void foo() {
        char *buf = malloc(MAX_SIZE);
        strncpy(buf, src, MAX_SIZE);
        fwrite(buf, MAX_SIZE, 1, fp);
        fclose(fp);
    }
    

    上面代码的说明如下:

    malloc的buf对应图中的application buffer。调用fwrite之后,操作系统将数据从application buffer拷贝到libc buffer,即c库标准IO buffer。fwrite 返回后,数据还保存在libc buffer,如果这个时候进程退出,这些数据将会丢失,没有写到磁盘上。

    当调用fclose的时候,fclose只会刷新libc buffer到page cache,如果确保数据写到磁盘,kernel buffer也必须flush。例如使用sync,fsync。除了fclose方法外,还有一个主动刷新接口fflush函数,但是fflush函数只是将数据从libc buffer拷贝到page cache,并没有刷新到磁盘上,从page cache刷新到磁盘上,可以通过调用fsync完成。

    sync会告诉OS,你要将这些数据刷新到磁盘上,但并不能真正保证确实已经写到磁盘上了。属于尽力而为的操作。( According to the standard specification (e.g., POSIX.1-2001), sync() schedules the writes, but may return before the actual writing is done. )sync会将page cache数据送到 disk cache层,至于什么时候写入物理磁盘介质上,则由磁盘控制器自己决定。

    从上面可以看到,一个常用的fwrite执行过程,需要经历多次数据拷贝才能最终到达磁盘。

    如果我们不想通过fwrite+fflush这种方式,而是想直接写到page cache。这就是我们linux常用的系统调用read/write函数。调用write函数时,实际上是直接通过系统调用将数据从application buffer拷贝到page cache中。但是我们知道,系统调用read/write会触发用户态到内核态的转换这将会一定性能消耗。

    如果我们想绕过page cache,直接将数据送到磁盘设备上怎么办?可以通过open的时候携带O_DIRECT属性。这时write该文件,就是直接写到设备上。

    如果我们想直接写到磁盘扇区有没有办法?这就是RAW设备写,绕开了文件系统,直接写扇区,例如:fdisk,dd之类的操作就是。

    3

    IO调用链

    fwrite是系统提供的最上层接口,也是最常用的接口。它在用户进程空间开辟了一个buffer,将多次小数据量相邻写操作先缓存起来,然后合并,最终调用write系统调用一次性写入(或者将大块数据分解多次write调用)。

    write函数通过调用系统调用接口,将数据从应用层拷贝到内核,因此write会触发用户态到内核态的切换。当数据到达page cache后,内核并不会立即将数据往下传递,而是返回用户空间。数据什么时候写入磁盘,是由内核IO调度决定,所以write是个异步调用。这一点和read不同,read调用先检查page cache里面是否有数据,如果有,就取回来并返回给用户,如果没有,就同步等待下去并等待有数据再返回给用户。所以read是个同步过程。如果想把write的异步过程改成同步过程,可以将open文件的时候,携带O_SYNC。

    数据到了page cache之后,内核有pdflush线程不停的检测脏页,判断是否要写回到磁盘中。然后把需要写回的页提交到IO队列(即:IO调度队列),由IO调用队列的调度策略决定何时写回。

    4

    IO调度层

    加入IO调度队列的任务并不一定会立即执行,调度层会从全局出发,尽量让整体磁盘IO性能最大化。大致的工作方式是让磁头类似电梯那样工作,先往一个方向走,走到尽头再回来,这样磁盘效率会比较高;磁盘是单向旋转的,不会反复逆时针转动,因为磁头寻道时间比较耗时。

    内核中有多种IO调度算法,例如:noop,deadline和cfg,在你机器上,通过dmesg | grep -i scheduler 命令可以查看你的linux支持的算法。当磁盘是SSD时,采用的是随机读写,并没有磁道、磁头,应用于传统机械磁盘的调度算法反而不适用。调度算法中的noop算法,适合配置SSD硬盘。

    任务从IO队列出来后,就到了驱动层,驱动层通过DMA,将数据写入磁盘cache。

    至于磁盘cache何时写入磁盘介质,这是由磁盘控制器自己决定。如果想确认要写到磁盘介质上,就调用fsync函数。

    5

    一致性和安全性

    5.1 安全性

    从上可以看到,数据没有到达磁盘介质之前,可能处于不同的物理内存cache中,那么这个时候如果出现进程退出,内核挂掉,物理机器掉电的情况,数据会丢失吗?

    • 当进程退出后:在application buffer或者libc buffer的数据会丢失。如果数据到了page cache,此时进程退出,即使数据还没有到达硬盘,数据也不会丢失。

    • 当内核挂掉:只要数据还没有到disk cache,都将会丢失。

    • 物理机掉电:此时所有数据都会丢失。

    5.2 一致性

    Q:同一个进程A中,多次open同一个文件,然后write数据会怎么样?

    fd1 = open("file", O_RDWR|O_TRUNC);
    fd2 = open("file", O_RDWR|O_TRUNC);
    
    
    while (1) {
        write(fd1, "hello 
    ", 6);
        write(fd2, "world 
    ", 6);
    }
    

    A:先写的数据是会被覆盖的。原因在于同一进程中不同的文件描述符(fd),各自对应一个独立的打开文件表,在打开文件表中有属于自己的文件位移量,开始都是0,然后各自从0开始写,每写一个字节向后移动一个字节,写的位置是重叠的,因此会覆盖。

    如何解决被覆盖的问题呢?

    必须为每个open指定O_APPEND。文件长度是共享的,当文件被写入数据后,文件长度就会被更新,指定O_APPEND之后,使用不同fd写数据时,都会使用文件长度更新自己的文件位移量,保证每次都是在文件的最末尾写数据,这样就不会出现覆盖。

    这里补充一点:同一个进程中多次open同一个文件时,文件描述符是不同的,在同一进程中某个文件描述符被占用,在close之前,是不会被再次分配。

    Q:两个进程A和进程B,open同一个文件,然后write数据会怎么样?

    A:先写的数据是会被覆盖的。覆盖的原因也是各自有独立的文件位移量。同样指定O_APPEND,使用文件长度更新文件位移量,保证各自操作时,都在文件尾部操作,不会出现相互覆盖的情况。

    这里补充一点:不同进程打开同一个文件时,各自使用的fd可能是相等的,之所以相同,是因为不同进程有自己单独的文件描述符池,默认是0~1023,各自分配各自的,是有可能分配到相等的fd

    Q:A进程写,B进程读取会写脏吗?

    A:会的。读取进程对文件内容变化毫无感知,只是按部就班的读取,直到文件结束EOF。

    5.3 读文件过程

    Linux read文件的流程大致如下:

    1. lib中的read函数首先进入系统调用sys_read;

    2. 接着sys_read再进入VFS中的vfs_read、generic_file_read等函数;

    3. 接着VFS中的generic_file_read会判断是否缓存命中,如果命中则返回;

    4. 如果没有命中,内核在page cache中分配一个新页框,发出缺页中断;

    5. 内核向通用块层发起IO请求,块设备屏蔽disk、U盘等差异;

    6. 通用块层将bio代表的IO请求发送到IO请求队列中;

    7. IO调度层通过电梯算法来调度队列中的请求;

    8. 驱动程序向磁盘控制器发出读取命令控制,DMA方式直接填充到page cache中的新页框;

    9. 控制器发出中断通知;

    10. 内核将用户需要的数据填充到用户内存中;

    11. 然后你的进程被唤醒;

    6

    性能问题

    磁盘的寻道时间时相当的慢,平均寻道大概在10ms,也就是每秒只能100-200次寻道。

    磁盘转速也是影响性能的关键,如果是15000rpm,大概每秒500转。一般情况下,盘片转太快,磁头跟不上,所以需要多转几圈才能完全读出磁道内容。

    机械硬盘顺序写 0~30MB左右,顺序读取速率一般 0~50MB左右,性能好的可以达到100多MB;SSD读取达到0~400MB左右。

    相关参考文章

    • http://blog.chinaunix.net/uid-27105712-id-3270102.html?page=2

    • https://zhuanlan.zhihu.com/p/138371910

    • https://meik2333.com/posts/linux-many-proc-write-file/

    • https://blog.csdn.net/qq_43648751/article/details/104151401

    
    关注「开源Linux」加星标,提升IT技能
    
    
    十年磨一剑
  • 相关阅读:
    Mooncake (排序+贪心)
    The Black Hole of Numbers (strtoint+inttostr+sort)
    随机排序
    EF 随机排序
    禁用DropDownList的Items
    sql之left join、right join、inner join的区别
    SQL优化-索引
    .net 面试问题 汇总
    从简单的例子理解泛型
    1.1 使用内置的Camera应用程序捕获图像
  • 原文地址:https://www.cnblogs.com/qinlulu/p/14671416.html
Copyright © 2020-2023  润新知