美文网首页CP-日常随笔
Linux操作系统I/O调度记录

Linux操作系统I/O调度记录

作者: CPinging | 来源:发表于2020-02-01 23:19 被阅读0次

    无情的搬用工

    使用缓冲区来提升性能。

    I/O调度

    buffer块缓冲区

    当一个块被调入内存的时候,它要存储在一个缓冲区中。每个缓冲区与一个块对应,一个页面可以容纳多个内存中的块。

    整体来说,Linux 文件缓冲区分为page cache和buffer cache,每一个 page cache 包含若干 buffer cache。

    具体文件系统则一般只与 buffer cache 交互,它们负责在存储设备和 buffer cache 之间交换数据,具体的文件系统直接操作的就是disk部分,而具体的怎么被包装被用户使用是VFS的责任(VFS将buffer cache包装成page给用户)。

    image.png

    对于具体的Linux文件系统,会以block(磁盘块)的形式组织文件,为了减少对物理块设备的访问,在文件以块的形式调入内存后,使用块高速缓存进行管理。每个缓冲区由两部分组成,第一部分称为缓冲区首部,用数据结构buffer_head表示,第二部分是真正的存储的数据。由于缓冲区首部不与数据区域相连,数据区域独立存储。

    具体操作流程
    hash表:用于管理包含有效数据的buffer,在定位buffer的时候很快捷。

    542         #define _hashfn(dev,block)      \
    543         ((((dev)<<(bh_hash_shift - 6)) ^ ((dev)<<(bh_hash_shift - 9))) ^ \
    544          (((block)<<(bh_hash_shift - 6)) ^ ((block) >> 13) ^ \
    545           ((block) << (bh_hash_shift - 12))))
    
    • 1 当我们在一个具有的文件系统中,当我们需要读取一块数据的时候,需要调用bread函数
    1189 struct buffer_head * bread(kdev_t dev, int block, int size)
    1190 {
    1191         struct buffer_head * bh;
    1192 
    1193         bh = getblk(dev, block, size);    /* 找到这个buffer */
    1194         if (buffer_uptodate(bh))  /* 判断是否存在有效数据,如果存在那么直接返回即可 */
    1195                 return bh;
    1196         set_bit(BH_Sync, &bh->b_state); /* 如果不存在有效数据,将这个buffer设置成同步状态 */
    1197         ll_rw_block(READ, 1, &bh); /* 如果没有,那么需要从磁盘中读取这个block到buffer中,这个是一个底层的读取操作 */
    1198         wait_on_buffer(bh);  /* 等待buffer的锁打开 */
    1199         if (buffer_uptodate(bh)) /* 理论上这个时候应该是存在有效数据的了,直接返回就可以 */
    1200                 return bh;
    1201         brelse(bh);
    1202         return NULL;
    1203 }
    

    第一:通过dev号+block号找到相应的buffer,使用函数getblk

    1013 struct buffer_head * getblk(kdev_t dev, int block, int size)
    1014 {
    1015         for (;;) {
    1016                 struct buffer_head * bh;
    1017 
    1018                 bh = get_hash_table(dev, block, size);   /* 关键函数,得到hash表中的buffer */
    1019                 if (bh) {
    1020                         touch_buffer(bh);
    1021                         return bh;     /* 返回这个buffer */
    1022                 }
    1023                 /* 如果没有找到对应的buffer,那么试着去增加一个buffer,就是使用下面的grow_buffers函数 */
    1024                 if (!grow_buffers(dev, block, size))
    1025                         free_more_memory();
    1026         }
    1027 }
    

    查找buffer函数:get_hash_table

    628 struct buffer_head * get_hash_table(kdev_t dev, int block, int size)
    629 {
    630         struct buffer_head *bh, **p = &hash(dev, block);   /* 首先通过hash值得到对应的位置,这个函数h很easy */
    631 /* 其实就是 #define hash(dev,block) hash_table[(_hashfn(HASHDEV(dev),block) & bh_hash_mask)]。hashfn函数上面已经说过了,就是通过hash值得到buffer*/
    632         read_lock(&hash_table_lock);
    633 
    634         for (;;) {           /* 下面就是判断这个得到的buffer数组中有没有我们需要的buffer */
    635                 bh = *p;
    636                 if (!bh)
    637                         break;
    638                 p = &bh->b_next;
    639                 if (bh->b_blocknr != block)
    640                         continue;
    641                 if (bh->b_size != size)
    642                         continue;
    643                 if (bh->b_dev != dev)
    644                         continue;
    645                 get_bh(bh);       /* 如果有那么直接执行这个函数,这个函数很easy,其实就是增加一个使用计数器而已: atomic_inc(&(bh)->b_count);*/
    646                 break;
    647         }
    648 
    649         read_unlock(&hash_table_lock);
    650         return bh;
    651 }
    

    如果没有查找到具体的值则调用grow_buffers

    2596 /*
    2597  * Try to increase the number of buffers available: the size argument
    2598  * is used to determine what kind of buffers we want.
    2599  */
    2600 static int grow_buffers(kdev_t dev, unsigned long block, int size)
    2601 {
    2602         struct page * page;
    2603         struct block_device *bdev;
    2604         unsigned long index;
    2605         int sizebits;
    2606 
    2607         /* Size must be multiple of hard sectorsize */
    2608         if (size & (get_hardsect_size(dev)-1))
    2609                 BUG();
    2610         /* Size must be within 512 bytes and PAGE_SIZE */
    2611         if (size < 512 || size > PAGE_SIZE)
    2612                 BUG();
    2613 
    2614         sizebits = -1;
    2615         do {
    2616                 sizebits++;
    2617         } while ((size << sizebits) < PAGE_SIZE);
    2618 
    2619         index = block >> sizebits;
    2620         block = index << sizebits;
    2621 
    2622         bdev = bdget(kdev_t_to_nr(dev));
    2623         if (!bdev) {
    2624                 printk("No block device for %s\n", kdevname(dev));
    2625                 BUG();
    2626         }
    2627 
    2628         /* Create a page with the proper size buffers.. */
    2629         page = grow_dev_page(bdev, index, size);
    2630 
    2631         /* This is "wrong" - talk to Al Viro */
    2632         atomic_dec(&bdev->bd_count);
    2633         if (!page)
    2634                 return 0;
    2635 
    2636         /* Hash in the buffers on the hash list */
    2637         hash_page_buffers(page, dev, block, size);
    2638         UnlockPage(page);
    2639         page_cache_release(page);
    2640 
    2641         /* We hashed up this page, so increment buffermem */
    2642         atomic_inc(&buffermem_pages);
    2643         return 1;
    2644 }
    2645 
    

    当前内核的块I/O操作均使用bio结构体表示。详细的内容不讲解,但是这个结构可以用来表示某个I/O操作所需要的块的位置,而这些块描述了每个片段在物理页中实际位置

    I/O调度

    把收到的IO请求进行合并以便最大化系统吞吐量。

    NOOP算法

    NOOP算法的全写为No Operation。该算法实现了最最简单的FIFO队列,所有IO请求大致按照先来后到的顺序进行操作。之所以说“大致”,原因是NOOP在FIFO的基础上还做了相邻IO请求的合并,并不是完完全全按照先进先出的规则满足IO请求。

    image.png

    NOOP是在Linux2。4或更早的版本的使用的唯一调度算法。由于该算法比较简单,减了IO占用CPU中的计算时间最少。不过容易造成IO请求饿死。关于IO饿死的描述如下:因为写请求比读请求更容易。写请求通过文件系统cache,不需要等一次写完成,就可以开始下一次写操作,写请求通过合并,堆积到I/O队列中。读请求需要等到它前面所有的读操作完成,才能进行下一次读操作。在读操作之间有几毫秒时间,而写请求在这之间就到来 ,饿死了后面的读请求 。

    CFQ算法

    该算法的特点是按照IO请求的地址进行排序,而不是按照先来后到的顺序来进行响应。CFQ为每个进程/线程,单独创建一个队列来管理该进程所产生的请求,也就是说每个进程一个队列,各队列之间的调度使用时间片来调度,以此来保证每个进程都能被很好的分配到I/O带宽。

    image.png

    在传统的SAS盘上,磁盘寻道花去了绝大多数的IO响应时间。CFQ的出发点是对IO地址进行排序,以尽量少的磁盘旋转次数来满足尽可能多的IO请求。在CFQ算法下,SAS盘的吞吐量大大提高了。但是相比于NOOP的缺点是,先来的IO请求并不一定能被满足,也可能会出现饿死的情况。

    DEADLINE算法

    最终期限算法(DEADLINE)在CFQ的基础上,解决了IO请求饿死的极端情况。除了CFQ本身具有的IO排序队列之外,DEADLINE额外分别为读IO和写IO提供了FIFO队列。读FIFO队列的最大等待时间为500ms,写FIFO队列的最大等待时间为5s。FIFO队列内的IO请求优先级要比CFQ队列中的高,而读FIFO队列的优先级又比写FIFO队列的优先级高。优先级可以表示如下:

    FIFO(Read) > FIFO(Write) > CFQ

    Deadline确保了在一个截止时间内服务请求,这个截止时间是可调整的,而默认读期限短于写期限。这样就防止了写操作因为不能被读取而饿死的现象。

    预先算法

    为了满足随机IO和顺序IO混合的场景,Linux还支持ANTICIPATORY调度算法。ANTICIPATORY的在DEADLINE的基础上,为每个读IO都设置了6ms的等待时间窗口。如果在这6ms内OS收到了相邻位置的读IO请求,就可以立即满足。

    本质上与Deadline一样,但在最后一次读操作后,要等待6ms,才能继续进行对其它I/O请求进行调度。可以从应用程序中预订一个新的读请求,改进读操作的执行,但以一些写操作为代价。它会在每个6ms中插入新的I/O操作,而会将一些小写入流合并成一个大写入流,用写入延时换取最大的写入吞吐量。AS适合于写入较多的环境,比如文件服务器,但对对数据库环境表现很差。

    避免了许多向后再向前的问题,所以6ms能换来很多优势还是很好的,如果不幸6ms内没有预期的操作,那么也就只是损失6ms而已。

    SSD的erase-before-write

    SSD的写操作比较特殊,SSD的最小写入单元为4KB,称为页(page),当写入空白位置时可以按照4KB的单位写入,但是如果需要改写某个单元时,则需要一个额外的擦除(erase)动作,如果向一个空白的page写入信息时,可以直接写入而无需擦除,但是如果需要改写某个存储单元(page)的数据,必须首先将整个block读入缓存,然后修改数据,并擦除整个block的数据,最后将整个block写入,很显然,SSD改写数据的代价很高,SSD的这个特性,我们称之为erase-before-write。

    SSD的erase-before-write

    SSD的写操作比较特殊,SSD的最小写入单元为4KB,称为页(page),当写入空白位置时可以按照4KB的单位写入,但是如果需要改写某个单元时,则需要一个额外的擦除(erase)动作,如果向一个空白的page写入信息时,可以直接写入而无需擦除,但是如果需要改写某个存储单元(page)的数据,必须首先将整个block读入缓存,然后修改数据,并擦除整个block的数据,最后将整个block写入,很显然,SSD改写数据的代价很高,SSD的这个特性,我们称之为erase-before-write。

    参考链接

    相关文章

      网友评论

        本文标题:Linux操作系统I/O调度记录

        本文链接:https://www.haomeiwen.com/subject/jqsxxhtx.html