什么是脏页?进程文件IO写操作,一般是把数据写入pagecache,此时这些pagecache就变成脏页。如果系统有多个进程同时进行文件IO写操作,系统脏页脏页会非常多,此时就要执行balance_dirty_pages()函数脏页平衡:唤醒脏页回写内核进程,并且文件IO写进程还要休眠一段时间,等待脏页刷回磁盘。
脏页回写的大体流程是什么?是否脏页太多就一定会触发脏页平衡?是否脏页太多进程就一定会休眠?为了解答这些疑问,需要清楚脏页回写相关函数balance_dirty_pages_ratelimited()和balance_dirty_pages()。本文内核源码版本3.10.96,详细内核源码注释见https://github.com/dongzhiyan-stack/kernel-code-comment。
1 脏页平衡源码解析
1.1 balance_dirty_pages_ratelimited()函数源码讲解
在正式介绍前,先看下内核文件IO写操作的一般流程:generic_file_aio_write->__generic_file_aio_write->generic_file_buffered_write->generic_perform_write-> balance_dirty_pages_ratelimited。可以发现在文件IO写操作的最后,总是执行generic_perform_write()->balance_dirty_pages_ratelimited()函数,看下balance_dirty_pages_ratelimited()函数源码:
- void balance_dirty_pages_ratelimited(struct address_space *mapping)
- {
- struct backing_dev_info *bdi = mapping->backing_dev_info;
- int ratelimit;
- int *p;
- if (!bdi_cap_account_dirty(bdi))
- return;
- //进程task结构的nr_dirtied_pause,即进程达到多少脏页时,进程需要执行balance_dirty_pages()进行脏页平衡,测试时current->nr_dirtied_pause有64,32,256
- ratelimit = current->nr_dirtied_pause;
- //如果已经执行balance_dirty_pages()进行脏页平衡,重新计算ratelimit,会很小(8),这样很容易执行下边的balance_dirty_pages()
- if (bdi->dirty_exceeded)
- ratelimit = min(ratelimit, 32 >> (PAGE_SHIFT - 10));//降低ratelimit,32 >> (PAGE_SHIFT - 10)=8
- preempt_disable();
- //在标记page脏页时执行account_page_dirtied()令bdp_ratelimits加1,表示当前cpu的脏页数
- p = &__get_cpu_var(bdp_ratelimits);//测试时bdp_ratelimits由1曾大到63还有256,进程脏页太多下边的if才成立
- if (unlikely(current->nr_dirtied >= ratelimit))
- *p = 0;//对per cpu变量bdp_ratelimits变量清0,这表示脏页太多,下边就要执行balance_dirty_pages进行脏页平衡了
- else if (unlikely(*p >= ratelimit_pages)) {//ratelimit_page初值32 ,实际1982,这里很少成立
- *p = 0;
- ratelimit = 0;
- }
- //进程退出时把进程残留的脏页数累加到dirty_throttle_leaks这个per cpu变量
- p = &__get_cpu_var(dirty_throttle_leaks);
- if (*p > 0 && current->nr_dirtied < ratelimit) {//测试时if很少成立,dirty_throttle_leaks基本是0,有时会大于0
- unsigned long nr_pages_dirtied;
- nr_pages_dirtied = min(*p, ratelimit - current->nr_dirtied);
- *p -= nr_pages_dirtied;
- current->nr_dirtied += nr_pages_dirtied;//增加当前进程脏页数nr_pages_dirtied
- }
- preempt_enable();
- //当前进程脏页数大于ratelimit才会执行balance_dirty_pages()进行脏页平衡
- if (unlikely(current->nr_dirtied >= ratelimit))
- balance_dirty_pages(mapping, current->nr_dirtied);
- }
当进程脏页数current->nr_dirtied超过current->nr_dirtied_pause这个阀值,才可能会执行最后的balance_dirty_pages(mapping, current->nr_dirtied)。current->nr_dirtied_pause测试时出现过有32、64、256,在balance_dirty_pages()函数中设置。current->nr_dirtied表示当前进程脏页数,在脏页数增加时执行account_page_dirtied()函数加1,如下:
- void account_page_dirtied(struct page *page, struct address_space *mapping)
- {
- trace_writeback_dirty_page(page, mapping);
- if (mapping_cap_account_dirty(mapping)) {
- //增加脏页NR_FILE_DIRTY
- __inc_zone_page_state(page, NR_FILE_DIRTY);
- __inc_zone_page_state(page, NR_DIRTIED);
- //BDI_RECLAIMABLE加1
- __inc_bdi_stat(mapping->backing_dev_info, BDI_RECLAIMABLE);
- //BDI_DIRTIED加1
- __inc_bdi_stat(mapping->backing_dev_info, BDI_DIRTIED);
- task_io_account_write(PAGE_CACHE_SIZE);
- current->nr_dirtied++;//current->nr_dirtied
- this_cpu_inc(bdp_ratelimits);
- }
- }
可以发现,这个函数还对NR_FILE_DIRTY脏页数、BDI_DIRTIED脏页数、BDI_RECLAIMABLE回收脏页数加1。
1.2 balance_dirty_pages()函数源码讲解
下边看下脏页平衡核心函数balance_dirty_pages()源码,有删减:
- static void balance_dirty_pages(struct address_space *mapping,
- unsigned long pages_dirtied)//pages_dirtied=current->nr_dirtied,当前进程脏页数
- {
- unsigned long nr_reclaimable; /* = file_dirty + unstable_nfs */
- unsigned long bdi_reclaimable;
- unsigned long nr_dirty; /* = file_dirty + writeback + unstable_nfs */
- unsigned long bdi_dirty;
- unsigned long freerun;
- unsigned long background_thresh;
- unsigned long dirty_thresh;
- unsigned long bdi_thresh;
- long period;
- long pause;
- long max_pause;
- long min_pause;
- int nr_dirtied_pause;
- bool dirty_exceeded = false;
- unsigned long task_ratelimit;
- unsigned long dirty_ratelimit;
- unsigned long pos_ratio;
- struct backing_dev_info *bdi = mapping->backing_dev_info;
- unsigned long start_time = jiffies;
- for (;;) {
- unsigned long now = jiffies;
- //nr_reclaimable文件脏页数,不包含正在回写的脏页数,这个包含 NR_UNSTABLE_NFS ???????????
- nr_reclaimable = global_page_state(NR_FILE_DIRTY) +
- global_page_state(NR_UNSTABLE_NFS);
- //nr_dirty是脏页数+正在回写的脏页数
- nr_dirty = nr_reclaimable + global_page_state(NR_WRITEBACK);
- //得到两个脏页阀值background_thresh和dirty_thresh,background_thresh脏页阀值与脏页回写内核进程有关(脏页超过阀值就回写),dirty_thresh脏页阀值与进程脏页平衡有关(进程会因脏页太多而阻塞)
- global_dirty_limits(&background_thresh, &dirty_thresh);
- //freerun=(dirty_thresh+background_thresh)/2
- freerun = dirty_freerun_ceiling(dirty_thresh,background_thresh);
- //如果进程脏页不多这里直接break了,不会休眠
- if (nr_dirty <= freerun) {
- current->dirty_paused_when = now;//更新进程的dirty_paused_when
- //每次执行balance_dirty_pages()函数都对current->nr_dirtied清0,这表示进行了脏页平衡所以对清0???????
- current->nr_dirtied = 0;
- current->nr_dirtied_pause =//测试时这里dirty_poll_interval()基本返回256
- dirty_poll_interval(nr_dirty, dirty_thresh);
- break;
- }
- //脏页太多而该bdi块设备脏页回写内核进程没运行,则唤醒bdi块设备脏页回写内核进程
- if (unlikely(!writeback_in_progress(bdi)))
- bdi_start_background_writeback(bdi);
- //根据dirty_thresh经过复杂的计算出bdi_thresh
- bdi_thresh = bdi_dirty_limit(bdi, dirty_thresh);
- if (bdi_thresh < 2 * bdi_stat_error(bdi)) {
- bdi_reclaimable = bdi_stat_sum(bdi, BDI_RECLAIMABLE);
- bdi_dirty = bdi_reclaimable +
- bdi_stat_sum(bdi, BDI_WRITEBACK);
- } else {
- //bdi脏页数
- bdi_reclaimable = bdi_stat(bdi, BDI_RECLAIMABLE);
- //bdi脏页数+bdi正在回写的脏页数
- bdi_dirty = bdi_reclaimable +
- bdi_stat(bdi, BDI_WRITEBACK);
- }
- /*bdi_dirty 和 nr_dirty 都超标则dirty_exceeded=1,bdi_dirty和nr_dirty有啥区别呢?测试脏页平衡休眠时,有时dirty_exceeded为1,有时dirty_exceeded是0*/
- dirty_exceeded = (bdi_dirty > bdi_thresh) &&
- (nr_dirty > dirty_thresh);
- //如果脏页超标则bdi->dirty_exceeded=1
- if (dirty_exceeded && !bdi->dirty_exceeded)
- bdi->dirty_exceeded = 1;
- //计算bdi->write_bandwidth和bdi->dirty_ratelimit,过程很复杂
- bdi_update_bandwidth(bdi, dirty_thresh, background_thresh,
- nr_dirty, bdi_thresh, bdi_dirty,
- start_time);
- dirty_ratelimit = bdi->dirty_ratelimit;
- //pos_ratio的计算是个玄学,与脏页数有关系。全局pos_ratio介于0~2之间,系统脏页nr_dirty越多pos_ratio越小,nr_dirty超过最大脏页阀值pos_ratio是0,这表示系统脏页太多了,进程要立即进行脏页平衡而休眠?
- pos_ratio = bdi_position_ratio(bdi, dirty_thresh,
- background_thresh, nr_dirty,
- bdi_thresh, bdi_dirty);
- //根据dirty_ratelimit和pos_ratio计算进程的task_ratelimit,task_ratelimit用来限制进程因脏页太多而休眠的时间。task_ratelimit = dirty_ratelimit *(pos_ratio/1024),pos_ratio/1024在0~2左右,是个比例值
- task_ratelimit = ((u64)dirty_ratelimit * pos_ratio) >>
- RATELIMIT_CALC_SHIFT;
- //根据bdi_dirty计算最大休眠时间mac_pause,bdi_dirty越大休眠时间越长
- max_pause = bdi_max_pause(bdi, bdi_dirty);
- //根据max_pause、task_ratelimit、dirty_ratelimit计算最小休眠时间
- min_pause = bdi_min_pause(bdi, max_pause,
- task_ratelimit, dirty_ratelimit,
- &nr_dirtied_pause);//进程脏页数超过nr_dirtied_pause就要进行脏页平衡
- //测试task_ratelimit会出现0,但是概率很低,此时脏页太多必须休眠
- if (unlikely(task_ratelimit == 0)) {
- period = max_pause;
- pause = max_pause;
- goto pause;
- }
- //period=当前进程脏页数pages_dirtied除以task_ratelimit,因脏页太多而要休眠的时间,与脏页数有关。与HZ相乘后,单位就和jiffies一样了
- period = HZ * pages_dirtied / task_ratelimit;
- //pause很重要,表示当前进程因为脏页太多而被迫休眠的时间
- pause = period;
- //current->dirty_paused_when是进程上一次执行balance_dirty_pages()脏页平衡的时间点(或者再加上休眠的时间点),now-current->dirty_paused_when这个时间差可能为正或者负数。时间差为正数时减少pause休眠时间,时间差为负数时增大pause休眠时间。
- if (current->dirty_paused_when)
- pause -= now - current->dirty_paused_when;
- //pause小于最小休眠时间min_pause直接break,休眠时间太小干脆不休眠了
- if (pause < min_pause) {
- ...............
- if (pause < -HZ) {
- //current->dirty_paused_when太小被更新为当前时间
- current->dirty_paused_when = now;
- //每次执行balance_dirty_pages()函数都对current->nr_dirtied清0,这表示进行了脏页平衡所以对清0???????
- current->nr_dirtied = 0;
- } else if (period) {//period >=-HZ且不为0
- current->dirty_paused_when += period;//current->dirty_paused_when
- //每次执行balance_dirty_pages()函数都对current->nr_dirtied清0,这表示进行了脏页平衡所以对清0???????
- current->nr_dirtied = 0;
- //这里在period为0时成立,
- } else if (current->nr_dirtied_pause <= pages_dirtied)
- //current->nr_dirtied_pause累加当前进程的脏页数pages_dirtied,越来越大
- current->nr_dirtied_pause += pages_dirtied;
- break;
- }
- //pause超过最大休眠时间则被赋值max_pause
- if (unlikely(pause > max_pause)) {
- /* for occasional dropped task_ratelimit */
- now += min(pause - max_pause, max_pause);
- pause = max_pause;
- }
- pause:
- __set_current_state(TASK_KILLABLE);
- //休眠pause毫秒
- io_schedule_timeout(pause);
- //current->dirty_paused_when这里记录脏页balance_dirty_pages的时间=当前时间+休眠时间,pause最大200ms。如果系统脏页太多,进程很快又执行到balance_dirty_pages()里的pause -= now-current->dirty_paused_when ,计算进程因为脏页太多休眠的时间,会出现now-current->dirty_paused_when是负数,导致pause很大,进程又要休眠。这样的目的应该是系统脏页太多了,进程多休眠。
- current->dirty_paused_when = now + pause;
- //每次执行balance_dirty_pages()函数都对current->nr_dirtied清0,这表示进行了脏页平衡所以对清0???????
- current->nr_dirtied = 0;
- current->nr_dirtied_pause = nr_dirtied_pause;//脏页太多休眠唤醒后current->nr_dirtied_pause赋初值
- ............
- }
- //脏页不超标了则对bdi->dirty_exceeded清0。这里有个理解误区,当进程1在balance_dirty_pages()函数前边bdi->dirty_exceeded=1置1,但是进程1执行到这里不会对bdi->dirty_exceeded=0清0,因为dirty_exceeded是1。需另外的进程执行到这里才会bdi->dirty_exceeded=0清0
- if (!dirty_exceeded && bdi->dirty_exceeded)
- bdi->dirty_exceeded = 0;
- ............
- }
这个流程有点繁琐,为了能突出脏页回写的重点,这里先把balance_dirty_pages()函数关键流程总结一下:
- 1 执行nr_dirty = nr_reclaimable + global_page_state(NR_WRITEBACK)计数系统文件脏页数nr_dirty,主要包括文件脏页数+正在回写的脏页数。
- 2 执行global_dirty_limits(&background_thresh, &dirty_thresh)计算脏页回下阀值background_thresh和dirty_thresh。脏页回写内核线程判定系统脏页数超过background_thresh这个阀值,才会执行wb_check_background_flush()进行脏页回写,详细见《writeback bdi脏页回写原理linux内核源码解析》这篇文章。
- 3 如果系统脏页太少if (nr_dirty <= freerun)成立,直接从balance_dirty_pages()函数返回,不会因为脏页而休眠。
- 4如果系统脏页太多,则第3步的if (nr_dirty <= freerun)不成立。继续向下执行,执行bdi_start_background_writeback(bdi) 唤醒bdi块设备脏页回写内核worker进程
- 5 执行bdi_thresh = bdi_dirty_limit(bdi, dirty_thresh)根据dirty_thresh计算bdi_thresh,这是另一个脏页阀值,与bdi块设备有关。执行bdi_dirty = bdi_reclaimable +bdi_stat(bdi, BDI_WRITEBACK)计算bdi_dirty。bdi_dirty只是当前bdi块设备的脏页, nr_dirty是系统总的脏页,二者不一样。
- 6 执行dirty_exceeded = (bdi_dirty > bdi_thresh) &&(nr_dirty > dirty_thresh)和 bdi->dirty_exceeded = 1,如果bdi脏页大于阀值,并且系统脏页数大于阀值则令bdi->dirty_exceeded = 1。
- 7 执行bdi_update_bandwidth(…..)根据各个脏页数及脏页阀值计算bdi->dirty_ratelimit,执行bdi_position_ratio(…..) 根据各个脏页数及脏页阀值计算pos_ratio,pos_ratio>>RATELIMIT_CALC_SHIFT介于0~2左右,是个比例值。接着执行task_ratelimit = ((u64)dirty_ratelimit * pos_ratio) >>RATELIMIT_CALC_SHIFT计算task_ratelimit,相当于令dirty_ratelimit乘以一个0~2之间的数字得到task_ratelimitbdi。bdi->dirty_ratelimit、pos_ratio、task_ratelimit计算流程也相当复杂。测试时发现,bdi->dirty_ratelimit在一段时间(200ms)是个固定值。
- 8 执行max_pause = bdi_max_pause(bdi, bdi_dirty)根据bdi_dirty计算进程因脏页平衡最大休眠时间。执行min_pause = bdi_min_pause(bdi, max_pause,task_ratelimit, dirty_ratelimit, &nr_dirtied_pause)根据计算max_pause、task_ratelimit、dirty_ratelimit进程因脏页平衡最小休眠时间。max_pause和min_pause的结果与系统脏页数、task_ratelimit、dirty_ratelimit紧密相关。
- 9 执行 period = HZ * pages_dirtied / task_ratelimit和pause = period根据当前进程脏页数和task_ratelimit,计算当前进程因脏页平衡而休眠的时间。还会执行pause -= now - current->dirty_paused_when对当前进程因脏页平衡而休眠的时间pause进行修正。now - current->dirty_paused_when可能是正数,也可能是负数,与进程上次执行balance_dirty_pages()进行脏页平衡有关。如果上次进程执行balance_dirty_pages()因为系统脏页太少直接从if (nr_dirty <= freerun)分支返回了,则current->dirty_paused_when记录当时的系统时间,此时now - current->dirty_paused_when是正数。而如果上次进程因脏页太多休眠,休眠唤醒后会执行current->dirty_paused_when = now + pause,令current->dirty_paused_when为当前系统时间加上休眠时间。如果pause很大,比如是200ms。很快的,进程IO写文件又执行balance_dirty_pages()脏页平衡,当前系统脏页依然超多。此时执行到pause -= now - current->dirty_paused_when,current->dirty_paused_when比当前系统时间now大概率大,故now - current->dirty_paused_when是负数,这样进行休眠时间pause就会增大。
- 10 如果if (pause < min_pause),说明进程要休眠的时间pause小于最小休眠时间min_pause,则针对pause< -HZ(if (pause < -HZ)分支)、period >=-HZ 且不为0(else if (period)分支)、period==0(else if (current->nr_dirtied_pause <= pages_dirtied)分支)单独做处理,然后balance_dirty_pages()就返回,当前进程不会休眠。
- 11 if (pause < min_pause)不成立时,进行要休眠的时间的pause很大,则执行io_schedule_timeout(pause)休眠pause毫秒,等待脏页刷回磁盘。然后执行current->dirty_paused_when = now + pause更新当前进程脏页平衡时间current->dirty_paused_when。current->dirty_paused_when等于当前系统时间加上休眠时间。还执行current->nr_dirtied = 0对当前进程脏页数清0,这里需要说明下,只要执行balance_dirty_pages()函数都对current->nr_dirtied清0(该函数有多处),这表示只要进行了脏页平衡就对current->nr_dirtied清0(我猜测的)。最后执行current->nr_dirtied_pause = nr_dirtied_pause更新当前进程进行脏页平衡的阀值。
我觉得balance_dirty_pages()函数最核心的一点就是计算进程因脏页平衡而休眠的时间pause。这里再总结一下这个流程:
- 1 计算系统脏页数nr_dirty、bdi脏页数bdi_dirty,计算脏页相关阀值background_thresh、dirty_thresh、bdi_thresh。
- 2 根据nr_dirty、bdi_dirty、background_thresh、dirty_thresh、bdi_thresh计算bdi->dirty_ratelimit、pos_ratio、task_ratelimit。计算因脏页平衡而休眠的最小和最大休眠时间min_pause、max_pause。
- 3 根据当前进程脏页数pages_dirtied(即current->nr_dirtied)和task_ratelimit计算因脏页平衡而休眠的时间pause。还会执行pause -= now - current->dirty_paused_when对pause做一定调整。之后如果pause>=min_pause则当前进程就要执行io_schedule_timeout(pause)休眠pause毫秒。
说实话这个流程很繁琐,尤其是bdi->dirty_ratelimit、pos_ratio、task_ratelimit的计算过程更甚。看源码涉及到了一些数学思想,计算原理还不太清楚。这3个变量控制进程脏页平衡而休眠的时间,系统认为当前进程需要休眠而等待脏页刷回磁盘,就要休眠。这里把计算过程再梳理下:
1.3 bdi->dirty_ratelimit、task_ratelimit更深层次讲解
首先提一点,每个块设备都有一个struct backing_dev_info *bdi结构体,其成员bdi->avg_write_bandwidth、bdi->write_bandwidth、bdi->dirty_ratelimit、bdi->balanced_dirty_ratelimit都与脏页平衡有关。他们的初值都是INIT_BW,在bdi块设备初始化中赋值INIT_BW,这是100M对应的page数。
- #define INIT_BW (100 << (20 - PAGE_SHIFT))// 100M对应的page数
- int bdi_init(struct backing_dev_info *bdi)
- {
- bdi->balanced_dirty_ratelimit = INIT_BW;
- bdi->dirty_ratelimit = INIT_BW;
- bdi->write_bandwidth = INIT_BW;
- bdi->avg_write_bandwidth = INIT_BW;
- }
感觉bdi->avg_write_bandwidth、bdi->write_bandwidth、bdi->dirty_ratelimit、bdi->balanced_dirty_ratelimit表示的是脏页有关的page数。在balance_dirty_pages->bdi_update_bandwidth->__bdi_update_bandwidth函数中计算更新这4个参数,看下源码:
- void __bdi_update_bandwidth(struct backing_dev_info *bdi,
- unsigned long thresh,
- unsigned long bg_thresh,
- unsigned long dirty,
- unsigned long bdi_thresh,
- unsigned long bdi_dirty,
- unsigned long start_time)
- {
- unsigned long now = jiffies;
- //bdi->bw_time_stamp是上次执行__bdi_update_bandwidth()计算bdi->dirty_ratelimit的系统时间,相减后elapsed需要大于200ms,才能再次计算bdi->dirty_ratelimit
- unsigned long elapsed = now - bdi->bw_time_stamp;
- unsigned long dirtied;
- unsigned long written;
- //每两次的时间间隔要大于200ms,否则直接返回
- if (elapsed < BANDWIDTH_INTERVAL)
- return;
- //当前bdi块设备的脏页数
- dirtied = percpu_counter_read(&bdi->bdi_stat[BDI_DIRTIED]);
- //当前bdi块设备已经回写的脏页数
- written = percpu_counter_read(&bdi->bdi_stat[BDI_WRITTEN]);
- if (elapsed > HZ && time_before(bdi->bw_time_stamp, start_time))
- goto snapshot;
- if (thresh) {
- global_update_bandwidth(thresh, dirty, now);
- //里边计算更新 bdi->dirty_ratelimit 和 bdi->balanced_dirty_ratelimit
- bdi_update_dirty_ratelimit(bdi, thresh, bg_thresh, dirty,
- bdi_thresh, bdi_dirty,
- dirtied, elapsed);
- }
- //这里计算 bdi->write_bandwidth和 bdi->avg_write_bandwidth
- bdi_update_write_bandwidth(bdi, elapsed, written);
- snapshot:
- bdi->dirtied_stamp = dirtied;//保存本次bdi块设备的脏页数
- bdi->written_stamp = written;//保存本次bdi块设备已经回写的脏页数
- bdi->bw_time_stamp = now;//记录 __bdi_update_bandwidth()函数中更新bdi->dirty_ratelimit的时间
- }
先看下bdi_update_write_bandwidth()函数:根据最近一段时间内bdi块设备回写的脏页数,计算bdi->write_bandwidth和bdi->avg_write_bandwidth。最近一段时间内bdi块设备回写的脏页数越多,bdi->write_bandwidth和bdi->avg_write_bandwidth越大,实际计算原理很复杂,源码如下:
- static void bdi_update_write_bandwidth(struct backing_dev_info *bdi,
- unsigned long elapsed,
- unsigned long written)//written是当前bdi块设备已经回写的脏页数
- {
- const unsigned long period = roundup_pow_of_two(3 * HZ);
- unsigned long avg = bdi->avg_write_bandwidth;
- unsigned long old = bdi->write_bandwidth;
- u64 bw;
- //bdi->written_stamp是上次执行__bdi_update_bandwidth()的bdi块设备回写脏页数,written是现在bdi块设备回写脏页数。二者相减是计算最近两次时间间隔内bdi块设备回写的脏页数
- bw = written - min(written, bdi->written_stamp);
- bw *= HZ;
- if (unlikely(elapsed > period)) {
- do_div(bw, elapsed);
- avg = bw;
- goto out;
- }
- //就是 bw * elapsed + write_bandwidth * (period - elapsed)
- bw += (u64)bdi->write_bandwidth * (period - elapsed);
- //就是 (bw * elapsed + write_bandwidth * (period - elapsed))/period
- bw >>= ilog2(period);
- //avg > old >bw,则avg减少(avg - old)/8,使得avg接近bw
- if (avg > old && old >= (unsigned long)bw)
- avg -= (avg - old) >> 3;
- //avg < old < bw,则avg增加(avg - old)/8,使得avg接近bw
- if (avg < old && old <= (unsigned long)bw)
- avg += (old - avg) >> 3;
- out:
- //bdi->write_bandwidth大致就表示这段时间单位时间内bdi块设备回写磁盘的脏页数吧?
- bdi->write_bandwidth = bw;
- //bdi->avg_write_bandwidth缓慢接近bdi->write_bandwidth
- bdi->avg_write_bandwidth = avg;
- }
接着看下bdi_update_dirty_ratelimit()函数,计算bdi->dirty_ratelimit和bdi->balanced_dirty_ratelimit。
- static void bdi_update_dirty_ratelimit(struct backing_dev_info *bdi,
- unsigned long thresh,
- unsigned long bg_thresh,
- unsigned long dirty,
- unsigned long bdi_thresh,
- unsigned long bdi_dirty,
- unsigned long dirtied,
- unsigned long elapsed)
- {
- //freerun=(thresh+bg_thresh)/2 最小脏页阀值
- unsigned long freerun = dirty_freerun_ceiling(thresh, bg_thresh);
- //limit是最大脏页阀值
- unsigned long limit = hard_dirty_limit(thresh);
- //setpoint是freerun和limit取半
- unsigned long setpoint = (freerun + limit) / 2;
- //write_bw=bdi->write_bandwidth大致表示前一次单位时间内bdi块设备回写磁盘的脏页数
- unsigned long write_bw = bdi->avg_write_bandwidth;
- unsigned long dirty_ratelimit = bdi->dirty_ratelimit;//前一次bdi块设备脏页限制速率
- unsigned long dirty_rate;
- unsigned long task_ratelimit;
- unsigned long balanced_dirty_ratelimit;
- unsigned long pos_ratio;
- unsigned long step;
- unsigned long x;
- //dirtied是bdi块设备当前的脏页数,bdi->dirtied_stamp是前一次执行bdi_update_dirty_ratelimit()计算bdi->dirty_ratelimit时的脏页数,elapsed这两次的时间差。二者相除计算出来的dirty_rate就是这段时间内该bdi块设备产生的脏页数,就是脏页产生速率吧。再乘以HZ是为了跟系统时间扯上关系吧,本质没啥意义。
- dirty_rate = (dirtied - bdi->dirtied_stamp) * HZ / elapsed;
- //pos_ratio的计算是个玄学,与脏页数有关系。全局pos_ratio介于0~2之间,系统脏页nr_dirty越多pos_ratio越小,nr_dirty超过最大脏页阀值pos_ratio是0,这表示系统脏页太多了,进程要立即进行脏页平衡而休眠。bdi pos_ratio的计算是个玄学,搞不清楚
- pos_ratio = bdi_position_ratio(bdi, thresh, bg_thresh, dirty,
- bdi_thresh, bdi_dirty);
- //dirty_ratelimit是上一次计算的 bdi->dirty_ratelimit,从而计算出task_ratelimit
- task_ratelimit = (u64)dirty_ratelimit *pos_ratio >> RATELIMIT_CALC_SHIFT;
- .........
- //write_bw与前一次单位时间内bdi块设备回写磁盘的脏页数有关,dirty_rate是前一次到这次时间内刷回磁盘的脏页数。balanced_dirty_ratelimit = task_ratelimit *(write_bw/dirty_rate),按照内核说明,这是为了计算对一个进程的脏页速率限制数,搞不清楚,dirty_rate跟写文件的进程数有啥关系???????
- balanced_dirty_ratelimit = div_u64((u64)task_ratelimit * write_bw,
- dirty_rate | 1);
- //balanced_dirty_ratelimit最大不能超过write_bw,write_bw与前一次计算的单位时间内bdi块设备回写磁盘的脏页数有关。就是说本次计算出来的balanced_dirty_ratelimit脏页平衡脏页速率限制page数,不能超过前一次单位时间内bdi块设备回写磁盘的脏页数,这是什么逻辑?
- if (unlikely(balanced_dirty_ratelimit > write_bw))
- balanced_dirty_ratelimit = write_bw;
- .............
- //dirty_ratelimit是在上一次的基础上增加或者减少step,从而更接近本次计算的balanced_dirty_ratelimit
- if (dirty_ratelimit < balanced_dirty_ratelimit)
- dirty_ratelimit += step;
- else
- dirty_ratelimit -= step;
- bdi->dirty_ratelimit = max(dirty_ratelimit, 1UL);
- bdi->balanced_dirty_ratelimit = balanced_dirty_ratelimit;
- }
这个计算过程涉及的数学思想挺负责的。我们只需理解一点,bdi->balanced_dirty_ratelimit跟脏页产生速率和脏页回写速率有关,bdi->dirty_ratelimit接近bdi->balanced_dirty_ratelimit。也就是说,bdi->dirty_ratelimit的计算原理很复杂,它与该bdi块设备的脏页产生速率和脏页回写速率有关,很复杂。之后再根据bdi->dirty_ratelimit计算task_ratelimit、max_pause、min_pause、脏页平衡休眠的时间period。再看下这个流程:
- static void balance_dirty_pages(struct address_space *mapping,
- unsigned long pages_dirtied)//pages_dirtied=current->nr_dirtied,当前进程脏页数
- {
- ..................
- //计算bdi->write_bandwidth和bdi->dirty_ratelimit,过程很复杂
- bdi_update_bandwidth(bdi, dirty_thresh, background_thresh,
- nr_dirty, bdi_thresh, bdi_dirty,
- start_time);
- //pos_ratio的计算是个玄学,与脏页数有关系。全局pos_ratio介于0~2之间,系统脏页nr_dirty越多pos_ratio越小,nr_dirty超过最大脏页阀值pos_ratio是0,这表示系统脏页太多了,进程要立即进行脏页平衡而休眠?
- pos_ratio = bdi_position_ratio(bdi, dirty_thresh,
- background_thresh, nr_dirty,
- bdi_thresh, bdi_dirty);
- //根据dirty_ratelimit和pos_ratio计算进程的task_ratelimit,task_ratelimit用来限制进程因脏页太多而休眠的时间。本质是task_ratelimit = dirty_ratelimit *(pos_ratio/1024)。pos_ratio/1024在0~2左右,是个比例值
- task_ratelimit = ((u64)dirty_ratelimit * pos_ratio) >>
- RATELIMIT_CALC_SHIFT;
- //根据bdi_dirty计算最大休眠时间max_pause
- max_pause = bdi_max_pause(bdi, bdi_dirty);
- //根据max_pause、task_ratelimit、dirty_ratelimit计算最小休眠时间
- min_pause = bdi_min_pause(bdi, max_pause,
- task_ratelimit, dirty_ratelimit,
- &nr_dirtied_pause);//进程脏页数超过nr_dirtied_pause就要阻塞休眠等待脏页回写
- .............
- //period=当前进程脏页数pages_dirtied除以task_ratelimit,因脏页太多而最大休眠时间
- period = HZ * pages_dirtied / task_ratelimit;
- }
简单来说,先根据当前bdi块设备脏页产生速率和脏页回写速率,经过繁琐的计算得到bdi->dirty_ratelimit,再计算出pos_ratio,根据二者计算出task_ratelimit。task_ratelimit感觉是为了限制进程产生脏页速率的,怎么限制呢?就是让进程休眠!接着,根据进程脏页数period = HZ * pages_dirtied / task_ratelimit计算进程因脏页平衡要休眠的时间。pages_dirtied是进程脏页数,显然进程脏页数越多period越大。并且bdi->dirty_ratelimit在bdi_update_dirty_ratelimit()中计算出已经与HZ挂钩,task_ratelimit当然也是,因此period = HZ * pages_dirtied / task_ratelimit的HZ就会抵消掉。
bdi_max_pause和bdi_min_pause的计算过程如下,计算也是个玄学
- static long bdi_min_pause(struct backing_dev_info *bdi,
- long max_pause,
- unsigned long task_ratelimit,
- unsigned long dirty_ratelimit,
- int *nr_dirtied_pause)
- {
- .........
- t = max(1, HZ / 100);//t这里表示进程因脏页平衡而休眠的时间,初值10ms,正常min_pause=t/2
- //这里计算出来pages就是nr_dirtied_pause,进程要进行脏页平衡的脏页阀值,与dirty_ratelimit这个脏页平衡限制速率page数和因脏页平衡而休眠的时间t成正比。
- pages = dirty_ratelimit * t / roundup_pow_of_two(HZ);
- .........
- //进程要进行脏页平衡的脏页上限
- *nr_dirtied_pause = pages;
- //t这里表示进程因脏页平衡而休眠的时间,nr_dirtied_pause大于128个page时,min_pause=t/2,否则min_pause=t.nr_dirtied_pause
- return pages >= DIRTY_POLL_THRESH ? 1 + t / 2 : t;
- }
- static unsigned long bdi_max_pause(struct backing_dev_info *bdi,
- unsigned long bdi_dirty)
- {
- unsigned long bw = bdi->avg_write_bandwidth;
- unsigned long t;
- //bdi_dirty脏页越多,bw与前一次单位时间内bdi块设备回写磁盘的脏页数有关,bw越少,计算出max_pause越大。就是说,脏页越多而bdi块设备回写磁盘的脏页数太少,则因脏页平衡而休眠的时间应该越大??????
- t = bdi_dirty / (1 + bw / roundup_pow_of_two(1 + HZ / 8));
- t++;
- return min_t(unsigned long, t, MAX_PAUSE);
- }
脏页平衡的计算到处是玄学(准确说是数学)!最后再把几个隐藏知识点列下:
1 系统脏页nr_dirty和bdi脏页bdi_dirty很多时,进程执行不一定执行到balance_dirty_pages()函数。因为balance_dirty_pages_ratelimited()函数中执行balance_dirty_pages()函数的条件是:current->nr_dirtied >= ratelimit,即当前进程脏页数要达到ratelimit阀值。就是说,当前进程脏页数很少时,current->nr_dirtied >= ratelimit不成立。
2 bdi->dirty_exceeded的变迁过程。举例,进程1执行到balance_dirty_pages()函数,如果bdi_dirty > bdi_thresh且nr_dirty > dirty_thresh(即bdi脏页数超过阀值且系统脏页数超过阀值),则执行bdi->dirty_exceeded = 1,之后进行1大概率在balance_dirty_pages()休眠。接着,如果进程2执行到balance_dirty_pages_ratelimited()函数,因为if (bdi->dirty_exceeded)成立,则执行ratelimit = min(ratelimit, 32 >> (PAGE_SHIFT - 10))调小ratelimit(ratelimit减少到 8)。这样if (unlikely(current->nr_dirtied >= ratelimit))很容易成立(毕竟进程只要有8个脏页就大于ratelimie),更容易执行balance_dirty_pages()函数进行脏页平衡。之所以这样设计,应该是内核认为当前系统已经有进程1在脏页平衡了,进程2 再进行文件IO写应该执行balance_dirty_pages()函数休眠,等待脏页回写一段时间再被唤醒。那bdi->dirty_exceeded什么时间被清0呢?等系统脏页不多了,进程3执行balance_dirty_pages()函数,dirty_exceeded = (bdi_dirty > bdi_thresh) &&(nr_dirty > dirty_thresh)被赋值0,然后到balance_dirty_pages()函数最后,执行bdi->dirty_exceeded = 0清0。
3 current->nr_dirtied_pause变量的变迁过程。balance_dirty_pages_ratelimited()函数中,如果进程脏页数超过current->nr_dirtied_pause才会执行balance_dirty_pages(……)进行脏页平衡。current->nr_dirtied_pause实际测试时遇到过0、16、32、64、256,它的更新在balance_dirty_pages(……)函数进行,计算过程也比较复杂………..。
2 脏页平衡的测试
怎样更容易触发脏页回写呢?调低脏页回写阀值:
- echo 5 > /proc/sys/vm/dirty_background_ratio
- echo 5 > /proc/sys/vm/dirty_ratio
让内核脏页回写进程执行的更慢
- echo 50000 > /proc/sys/vm/dirty_expire_centisecs
- echo 50000 > /proc/sys/vm/dirty_expire_centisecs
同时启动N多个dd进程进行IO文件写
- [root@localhost ~]# dd if=/dev/zero of=test2 bs=1K count=20000000
- [root@localhost ~]# dd if=/dev/zero of=test3 bs=1K count=20000000
- [root@localhost ~]# dd if=/dev/zero of=test6 bs=1K count=20000000
这样就很容易触发脏页平衡执行到balance_dirty_pages()函数,然后休眠。这里把balance_dirty_pages()函数测试时的关键数据梳理了一下,把截图发下:
第A列是dd进程因为脏页太多执行balance_dirty_pages()函数时,打印的关键变量名称,之后的每一列就是这些变量的实际数据。可以发现几个规律:
- 1 系统脏页很多时,进程执行到balance_dirty_pages()函数并不一定休眠。比如第C和D列的数据,因为pause<min_pause。
- 2 current->dirty_paused_when确实比当前系统时间大,比如第B的数据。
- 3 pos_ratio、task_ratelimit、nr_dirtied_pause可能为0