无情的搬用工
使用缓冲区来提升性能。
I/O调度
buffer块缓冲区
当一个块被调入内存的时候,它要存储在一个缓冲区中。每个缓冲区与一个块对应,一个页面可以容纳多个内存中的块。
整体来说,Linux 文件缓冲区分为page cache和buffer cache,每一个 page cache 包含若干 buffer cache。
具体文件系统则一般只与 buffer cache 交互,它们负责在存储设备和 buffer cache 之间交换数据,具体的文件系统直接操作的就是disk部分,而具体的怎么被包装被用户使用是VFS的责任(VFS将buffer cache包装成page给用户)。
对于具体的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请求。
NOOP是在Linux2。4或更早的版本的使用的唯一调度算法。由于该算法比较简单,减了IO占用CPU中的计算时间最少。不过容易造成IO请求饿死。关于IO饿死的描述如下:因为写请求比读请求更容易。写请求通过文件系统cache,不需要等一次写完成,就可以开始下一次写操作,写请求通过合并,堆积到I/O队列中。读请求需要等到它前面所有的读操作完成,才能进行下一次读操作。在读操作之间有几毫秒时间,而写请求在这之间就到来 ,饿死了后面的读请求 。
CFQ算法
该算法的特点是按照IO请求的地址进行排序,而不是按照先来后到的顺序来进行响应。CFQ为每个进程/线程,单独创建一个队列来管理该进程所产生的请求,也就是说每个进程一个队列,各队列之间的调度使用时间片来调度,以此来保证每个进程都能被很好的分配到I/O带宽。
在传统的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文件系统(五)---三大缓冲区之buffer块缓冲区
https://blog.csdn.net/shanshanpt/article/details/39258373linux IO调度(电梯算法)
https://blog.csdn.net/substitute_coder/article/details/79861463