我们前一章节中学习Linux是用Cache/Buffer缓存数据,提高系统的I/O性能,且有一个回刷任务在适当时候把脏数据回刷到储存介质中。那么本章重点学习优化机制,包括以下内容
什么时候触发回刷?
脏数据达到多少阈值还是定时触发呢?
内核是如何做到回写机制的
1. 配置概述
Linux内核在/proc/sys/vm中有透出数个配置文件,可以对触发回刷的时机进行调整。内核的回刷进程是怎么运作的呢?这数个配置文件有什么作用呢?
root@public-VirtualBox:~# sysctl -a | grep dirty
vm.dirty_background_bytes = 0
vm.dirty_background_ratio = 10
vm.dirty_bytes = 0
vm.dirty_expire_centisecs = 3000
vm.dirty_ratio = 20
vm.dirty_writeback_centisecs = 500
vm.dirtytime_expire_seconds = 43200
1
2
3
4
5
6
7
8
在/proc/sys/vm中有以下文件与回刷脏数据密切相关:
配置文件 功能 默认值
vm.dirty_background_ratio 触发回刷的脏数据占用内存的百分比 10
vm.dirty_background_bytes 触发回刷的脏数据量 0
vm.dirty_bytes 触发同步写的脏数据量 0
vm.dirty_ratio 触发同步写的脏数据占可用内存的百分比 20
vm.dirty_expire_centisecs 脏数据超时回刷时间(单位:1/100S) 3000
vm.dirty_writeback_centisecs 回刷进程定时唤醒时间(单位: 1/100S) 500
vm.dirty_background_ratio:
内存可以填充脏数据的百分比,这些脏数据稍后会写入磁盘。pdflush/flush/kdmflush这些后台进程会稍后清理脏数据。比如,我有32G内存,那么有3.2G(10%的比例)的脏数据可以待着内存里,超过3.2G的话就会有后台进程来清理。
vm.dirty_ratio
可以用脏数据填充的绝对最大系统内存量,当系统到达此点时,必须将所有脏数据提交到磁盘,同时所有新的I/O块都会被阻塞,直到脏数据被写入磁盘。这通常是长I/O卡顿的原因,但这也是保证内存中不会存在过量脏数据的保护机制。
vm.dirty_background_bytes 和 vm.dirty_bytes
另一种指定这些参数的方法。如果设置 xxx_bytes版本,则 xxx_ratio版本将变为0,反之亦然。
vm.dirty_expire_centisecs
指定脏数据能存活的时间。在这里它的值是30秒。当 pdflush/flush/kdmflush 在运行的时候,他们会检查是否有数据超过这个时限,如果有则会把它异步地写到磁盘中。毕竟数据在内存里待太久也会有丢失风险。
vm.dirty_writeback_centisecs
指定多长时间 pdflush/flush/kdmflush 这些进程会唤醒一次,然后检查是否有缓存需要清理。
实际上dirty_ratio的数字大于dirty_background_ratio,是不是就不会达到dirty_ratio呢?
首先达到dirty_background_ratio的条件后触发flush进程进行异步的回写操作,但是这一过程中应用进程仍然可以进行写操作,如果多个应用写入的量大于flush进程刷出的量,那自然就会达到vm.dirty_ratio这个参数所设定的阙值,此时操作系统会转入同步地进行脏页的过程,阻塞应用进程。
2. 配置实例
单纯的配置说明毕竟太抽象。结合网上的分享,我们看看在不同场景下,该如何配置?
场景1:尽可能不丢数据
有些产品形态的数据非常重要,例如行车记录仪。在满足性能要求的情况下,要做到尽可能不丢失数据。
/* 此配置不一定适合您的产品,请根据您的实际情况配置 */
dirty_background_ratio = 5
dirty_ratio = 10
dirty_writeback_centisecs = 50
dirty_expire_centisecs = 100
这样的配置有以下特点:
当脏数据达到可用内存的5%时唤醒回刷进程
脏数据达到可用内存的10%时,应用每一笔数据都必须同步等待
每隔500ms唤醒一次回刷进程
当脏数据达到可用内存的5%时唤醒回刷进程
由于发生交通事故时,行车记录仪随时可能断电,事故前1~2s的数据尤为关键。因此在保证性能满足不丢帧的情况下,尽可能回刷数据。
此配置通过减少Cache,更加频繁唤醒回刷进程的方式,尽可能让数据回刷。
此时的性能理论上会比每笔数据都O_SYNC略高,比默认配置性能低,相当于用性能换数据安全。
场景2:追求更高性能
有些产品形态不太可能会掉电,例如服务器。此时不需要考虑数据安全问题,要做到尽可能高的IO性能。
/* 此配置不一定适合您的产品,请根据您的实际情况配置 */
dirty_background_ratio = 50
dirty_ratio = 80
dirty_writeback_centisecs = 2000
dirty_expire_centisecs = 12000
这样的配置有以下特点:
当脏数据达到可用内存的50%时唤醒回刷进程
当脏数据达到可用内存的80%时,应用每一笔数据都必须同步等待
每隔20s唤醒一次回刷进程
内存中脏数据存在时间超过120s则在下一次唤醒时回刷
与场景1相比,场景2的配置通过 增大Cache,延迟回刷唤醒时间来尽可能缓存更多数据,进而实现提高性能
场景3:突然的IO峰值拖慢整体性能
什么是IO峰值?突然间大量的数据写入,导致瞬间IO压力飙升,导致瞬间IO性能狂跌,对行车记录仪而言,有可能触发视频丢帧。
/* 此配置不一定适合您的产品,请根据您的实际情况配置 */
dirty_background_ratio = 5
dirty_ratio = 80
dirty_writeback_centisecs = 500
dirty_expire_centisecs = 3000
1
2
3
4
5
这样的配置有以下特点:
当脏数据达到可用内存的5%时唤醒回刷进程
当脏数据达到可用内存的80%时,应用每一笔数据都必须同步等待
每隔5s唤醒一次回刷进程
内存中脏数据存在时间超过30s则在下一次唤醒时回刷
这样的配置,通过增大Cache总容量,更加频繁唤醒回刷的方式,解决IO峰值的问题,此时能保证脏数据比例保持在一个比较低的水平,当突然出现峰值,也有足够的Cache来缓存数据。
3. 内核演变
对于回写方式在之前的2.4内核中,使用 bdflush的线程专门负责writeback的操作,因为磁盘I/O操作很慢,而现代操作系统通常具有多个块设备,如果bdflush在其中一个块设备上等待I/O操作的完成,可能需要很长的时间,此时其他块设备还处于空闲状态,这时候,单线程模式的bdflush就称为了影响性能的瓶颈。而此时bdflush是没有周期扫描功能,因此需要配合kupdate线程一起使用。
bdflush 存在的问题:
整个系统仅仅只有一个 bdflush 线程,当系统回写任务较重时,bdflush 线程可能会阻塞在某个磁盘的I/O上,
导致其他磁盘的I/O回写操作不能及时执行
于是在2.6内核中,bdflush机制就被pdflush取代,pdflush是一组线程,根据块设备I/O负载情况,数量从最少的2个到最多的8个不等,如果1S内都没有空闲的pdflush线程可用,内核将创建一个新的pdflush线程,反之某个pdflush线程空闲超过1S,则该线程将会被销毁。pdflush 线程数目是动态的,取决于系统的I/O负载。它是面向系统中所有磁盘的全局任务的。
pdflush 存在的问题:
pdflush的数目是动态的,一定程度上缓解了 bdflush 的问题。但是由于 pdflush 是面向所有磁盘的,所以有可能出现多个 pdflush 线程全部阻塞在某个拥塞的磁盘上,同样导致其他磁盘的I/O回写不能及时执行。
于是在内最新的内核中,直接将一个块设备对应一个thread,这种内核线程被称为flusher threads,线程名为“Writeback",执行体为"wb_workfn",通过workqueue机制实现调度。
4. 内核实现
由于内核page cache的作用,写操作实际被延迟写入。当page cache里的数据被用户写入但是没有刷新到磁盘时,则该page为脏页(块设备page cache机制因为以前机械磁盘以扇区为单位读写,引入了buffer_head,每个4K的page进一步划分成8个buffer,通过buffer_head管理,因此可能只设置了部分buffer head为脏)。
脏页在以下情况下将被回写(write back)到磁盘上:
脏页在内存里的时间超过了阈值。
系统的内存紧张,低于某个阈值时,必须将所有脏页回写。
用户强制要求刷盘,如调用sync()、fsync()、close()等系统调用。
以前的Linux通过pbflush机制管理脏页的回写,但因为其管理了所有的磁盘的page/buffer_head,存在严重的性能瓶颈,因此从Linux 2.6.32开始,脏页回写的工作由bdi_writeback机制负责。bdi_writeback机制为每个磁盘都创建一个线程,专门负责这个磁盘的page cache或者buffer cache的数据刷新工作,以提高I/O性能。
在 kernel/sysctl.c中列出了所有的配置文件的信息
static struct ctl_table vm_table[] = {
...
{
.procname = "dirty_background_ratio",
.data = &dirty_background_ratio,
.maxlen = sizeof(dirty_background_ratio),
.mode = 0644,
.proc_handler = dirty_background_ratio_handler,
.extra1 = &zero,
.extra2 = &one_hundred,
},
{
.procname = "dirty_ratio",
.data = &vm_dirty_ratio,
.maxlen = sizeof(vm_dirty_ratio),
.mode = 0644,
.proc_handler = dirty_ratio_handler,
.extra1 = &zero,
.extra2 = &one_hundred,
},
{
.procname = "dirty_writeback_centisecs",
.data = &dirty_writeback_interval,
.maxlen = sizeof(dirty_writeback_interval),
.mode = 0644,
.proc_handler = dirty_writeback_centisecs_handler,
},
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
这些值在mm/page-writeback.c中有全局变量定义
int dirty_background_ratio = 10;
int vm_dirty_ratio = 20;
unsigned int dirty_writeback_interval = 5 * 100; /* centiseconds */
1
2
3
通过ps -aux,我们可以看到writeback的内核进程
这实际上是一个工作队列对应的进程,在default_bdi_init()中创建(mm/backing-dev.c)
static int __init default_bdi_init(void)
{
int err;
bdi_wq = alloc_workqueue("writeback", WQ_MEM_RECLAIM | WQ_FREEZABLE |
WQ_UNBOUND | WQ_SYSFS, 0);
if (!bdi_wq)
return -ENOMEM;
err = bdi_init(&noop_backing_dev_info);
return err;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
回刷进程的核心是函数wb_workfn(),通过函数wb_init()绑定。
static int wb_init(struct bdi_writeback *wb, struct backing_dev_info *bdi
int blkcg_id, gfp_t gfp)
{
...
INIT_DELAYED_WORK(&wb->dwork, wb_workfn);
...
}
1
2
3
4
5
6
7
唤醒回刷进程的操作是这样的
static void wb_wakeup(struct bdi_writeback *wb)
{
spin_lock_bh(&wb->work_lock);
if (test_bit(WB_registered, &wb->state))
mod_delayed_work(bdi_wq, &wb->dwork, 0);
spin_unlock_bh(&wb->work_lock);
}
1
2
3
4
5
6
7
表示唤醒的回刷任务在工作队列writeback中执行,这样,就把工作队列和回刷工作绑定了,重点看看这个接口做了些什么工作
void wb_workfn(struct work_struct *work)
{
struct bdi_writeback *wb = container_of(to_delayed_work(work),
struct bdi_writeback, dwork);
long pages_written;
set_worker_desc("flush-%s", dev_name(wb->bdi->dev));
current->flags |= PF_SWAPWRITE;
//如果当前不是一个救援工作队列,或者当前bdi设备已注册,这是一般路径
if (likely(!current_is_workqueue_rescuer() ||
!test_bit(WB_registered, &wb->state))) { ------------------------(1)
/*
* The normal path. Keep writing back @wb until its
* work_list is empty. Note that this path is also taken
* if @wb is shutting down even when we're running off the
* rescuer as work_list needs to be drained.
*/
do {从bdi的work_list取出队列里的任务,执行脏页回写
pages_written = wb_do_writeback(wb);
trace_writeback_pages_written(pages_written);
} while (!list_empty(&wb->work_list));
} else { -----------------------(2)
/*
* bdi_wq can't get enough workers and we're running off
* the emergency worker. Don't hog it. Hopefully, 1024 is
* enough for efficient IO.
*/
pages_written = writeback_inodes_wb(wb, 1024,//只提交一个work并限制写入1024个pages
WB_REASON_FORKER_THREAD);
trace_writeback_pages_written(pages_written);
}
//如果上面处理完到现在这段间隔又有了work,再次立马启动回写进程
if (!list_empty(&wb->work_list)) -----------------------(3)
mod_delayed_work(bdi_wq, &wb->dwork, 0);
else if (wb_has_dirty_io(wb) && dirty_writeback_interval)
wb_wakeup_delayed(wb);//如果所有bdi设备上挂的dirty inode回写完,那么就重置定制器,
//再过dirty_writeback_interval,即5s后再唤醒回写进程
current->flags &= ~PF_SWAPWRITE;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
正常路径,rescue workerrescue内核线程,内存紧张时创建新的工作线程可能会失败,如果内核中有会需要回收的内存,就调用wb_do_writeback进行回收
如果当前workqueue不能获得足够的worker进行处理,只提交一个work并限制写入1024个pages
这也过程代码较多,暂不去深入分析,重点关注相关的配置是如何起作用的。
5. 触发回写方式
触发writeback的地方主要有以下几处:
5.1 主动发起
手动执行sysn命令
sync->
SYSCALL_DEFINE0(sync)->
sync_inodes_one_sb->
sync_inodes_sb->
bdi_queue_work
1
2
3
4
5
syncfs系统调用
SYSCALL_DEFINE1(syncfs, int, fd)->
sync_filesystem->
__sync_filesystem->
sync_inodes_sb->
bdi_queue_work
1
2
3
4
5
直接内存回收,内存不足时调用
free_more_memory->
wakeup_flusher_threads->
__bdi_start_writeback->
bdi_queue_work
1
2
3
4
分配内存空间不足,触发回写脏页腾出内存空间
__alloc_pages_nodemask->
__alloc_pages_slowpath->
__alloc_pages_direct_reclaim->
__perform_reclaim->
try_to_free_pages->
do_try_to_free_pages->
wakeup_flusher_threads->
__bdi_start_writeback->
bdi_queue_work
1
2
3
4
5
6
7
8
9
remount/umount操作,需要先将脏页写回
5.2 空间层面
当系统的“dirty”的内存大于某个阈值,该阈值是在总共的“可用内存”(包括free pages 和reclaimable pages)中的占比。
参数“dirty_background_ratio”(默认值10%),或者是绝对字节数“dirty_background_bytes”(默认值为0,表示生效)。两个参数只要谁先达到即可执行,此时就会交给专门负责writeback的background线程去处理。
参数“dirty_ratio”(默认值30%)和“dirty_bates”(默认值为0,表示生效),当“dirty”的内存达到这个比例或数量,进程则会停下write操作(被阻塞),先把“dirty”进行writeback。
5.3 时间层面
周期性的扫描,扫描间隔用参数:dirty_writeback_interval表示,以毫秒为单位。发现存在最近一次更新时间超过某个阈值(参数:dirty_expire_interval,单位毫秒)的pages。如果每个page都维护最近更新时间,开销会很大且扫描会很耗时,因此具体实现不会以page为粒度,而是按inode中记录的dirtying-time来计算。
6. 总结
文件缓存是一项重要的性能改进,在大多数情况下,读缓存在绝大多数情况下是有益无害的(程序可以直接从RAM中读取数据)。写缓存比较复杂,Linux内核将磁盘写入缓存,过段时间再异步将它们刷新到磁盘。这对加速磁盘I/O有很好的效果,但是当数据未写入磁盘时,丢失数据的可能性会增加。
————————————————
版权声明:本文为CSDN博主「奇小葩」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u012489236/article/details/115579851