内核脏页平衡 balance_dirty_pages()源码解析

什么是脏页?进程文件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()函数源码:

  1. void balance_dirty_pages_ratelimited(struct address_space *mapping)
  2. {
  3.     struct backing_dev_info *bdi = mapping->backing_dev_info;
  4.     int ratelimit;
  5.     int *p;
  6.     if (!bdi_cap_account_dirty(bdi))
  7.         return;
  8.     //进程task结构的nr_dirtied_pause,即进程达到多少脏页时,进程需要执行balance_dirty_pages()进行脏页平衡,测试时current->nr_dirtied_pause6432256
  9.     ratelimit = current->nr_dirtied_pause;
  10.     //如果已经执行balance_dirty_pages()进行脏页平衡,重新计算ratelimit,会很小(8),这样很容易执行下边的balance_dirty_pages()
  11.     if (bdi->dirty_exceeded)
  12.         ratelimit = min(ratelimit, 32 >> (PAGE_SHIFT - 10));//降低ratelimit32 >> (PAGE_SHIFT - 10)=8
  13.     preempt_disable();
  14.     //在标记page脏页时执行account_page_dirtied()bdp_ratelimits1,表示当前cpu的脏页数
  15.     p =  &__get_cpu_var(bdp_ratelimits);//测试时bdp_ratelimits1曾大到63还有256,进程脏页太多下边的if才成立
  16.     if (unlikely(current->nr_dirtied >= ratelimit))
  17.         *p = 0;//per cpu变量bdp_ratelimits变量清0,这表示脏页太多,下边就要执行balance_dirty_pages进行脏页平衡了
  18.     else if (unlikely(*p >= ratelimit_pages)) {//ratelimit_page初值32 ,实际1982,这里很少成立
  19.         *p = 0;
  20.         ratelimit = 0;
  21.     }
  22.     //进程退出时把进程残留的脏页数累加到dirty_throttle_leaks这个per cpu变量
  23.     p = &__get_cpu_var(dirty_throttle_leaks);
  24.     if (*p > 0 && current->nr_dirtied < ratelimit) {//测试时if很少成立,dirty_throttle_leaks基本是0,有时会大于0
  25.         unsigned long nr_pages_dirtied;
  26.         nr_pages_dirtied = min(*p, ratelimit - current->nr_dirtied);
  27.         *p -= nr_pages_dirtied;
  28.         current->nr_dirtied += nr_pages_dirtied;//增加当前进程脏页数nr_pages_dirtied
  29.     }
  30.     preempt_enable();
  31.     //当前进程脏页数大于ratelimit才会执行balance_dirty_pages()进行脏页平衡
  32.     if (unlikely(current->nr_dirtied >= ratelimit))
  33.         balance_dirty_pages(mapping, current->nr_dirtied);
  34. }

当进程脏页数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,如下:

  1. void account_page_dirtied(struct page *page, struct address_space *mapping)
  2. {
  3.     trace_writeback_dirty_page(page, mapping);
  4.     if (mapping_cap_account_dirty(mapping)) {
  5.         //增加脏页NR_FILE_DIRTY
  6.         __inc_zone_page_state(page, NR_FILE_DIRTY);
  7.         __inc_zone_page_state(page, NR_DIRTIED);
  8.         //BDI_RECLAIMABLE1
  9.         __inc_bdi_stat(mapping->backing_dev_info, BDI_RECLAIMABLE);
  10.         //BDI_DIRTIED1
  11.         __inc_bdi_stat(mapping->backing_dev_info, BDI_DIRTIED);
  12.         task_io_account_write(PAGE_CACHE_SIZE);
  13.         current->nr_dirtied++;//current->nr_dirtied
  14.         this_cpu_inc(bdp_ratelimits);
  15.     }
  16. }

可以发现,这个函数还对NR_FILE_DIRTY脏页数、BDI_DIRTIED脏页数、BDI_RECLAIMABLE回收脏页数加1。

1.2 balance_dirty_pages()函数源码讲解

下边看下脏页平衡核心函数balance_dirty_pages()源码,有删减:

  1. static void balance_dirty_pages(struct address_space *mapping,
  2.                 unsigned long pages_dirtied)//pages_dirtied=current->nr_dirtied,当前进程脏页数
  3. {
  4.     unsigned long nr_reclaimable;   /* = file_dirty + unstable_nfs */
  5.     unsigned long bdi_reclaimable;
  6.     unsigned long nr_dirty;  /* = file_dirty + writeback + unstable_nfs */
  7.     unsigned long bdi_dirty;
  8.     unsigned long freerun;
  9.     unsigned long background_thresh;
  10.     unsigned long dirty_thresh;
  11.     unsigned long bdi_thresh;
  12.     long period;
  13.     long pause;
  14.     long max_pause;
  15.     long min_pause;
  16.     int nr_dirtied_pause;
  17.     bool dirty_exceeded = false;
  18.     unsigned long task_ratelimit;
  19.     unsigned long dirty_ratelimit;
  20.     unsigned long pos_ratio;
  21.     struct backing_dev_info *bdi = mapping->backing_dev_info;
  22.     unsigned long start_time = jiffies;
  23.     for (;;) {
  24.         unsigned long now = jiffies;
  25.         //nr_reclaimable文件脏页数,不包含正在回写的脏页数,这个包含 NR_UNSTABLE_NFS ???????????
  26.         nr_reclaimable = global_page_state(NR_FILE_DIRTY) +
  27.                     global_page_state(NR_UNSTABLE_NFS);
  28.         //nr_dirty是脏页数+正在回写的脏页数       
  29.         nr_dirty = nr_reclaimable + global_page_state(NR_WRITEBACK);
  30.         //得到两个脏页阀值background_threshdirty_threshbackground_thresh脏页阀值与脏页回写内核进程有关(脏页超过阀值就回写)dirty_thresh脏页阀值与进程脏页平衡有关(进程会因脏页太多而阻塞)
  31.         global_dirty_limits(&background_thresh, &dirty_thresh);
  32.         //freerun=(dirty_thresh+background_thresh)/2
  33.         freerun = dirty_freerun_ceiling(dirty_thresh,background_thresh);
  34.        
  35.         //如果进程脏页不多这里直接break了,不会休眠
  36.         if (nr_dirty <= freerun) {
  37.             current->dirty_paused_when = now;//更新进程的dirty_paused_when
  38.             //每次执行balance_dirty_pages()函数都对current->nr_dirtied0,这表示进行了脏页平衡所以对清0???????
  39.             current->nr_dirtied = 0;
  40.             current->nr_dirtied_pause =//测试时这里dirty_poll_interval()基本返回256
  41.                 dirty_poll_interval(nr_dirty, dirty_thresh);
  42.             break;
  43.         }
  44.         //脏页太多而该bdi块设备脏页回写内核进程没运行,则唤醒bdi块设备脏页回写内核进程
  45.         if (unlikely(!writeback_in_progress(bdi)))
  46.             bdi_start_background_writeback(bdi);
  47.         //根据dirty_thresh经过复杂的计算出bdi_thresh
  48.         bdi_thresh = bdi_dirty_limit(bdi, dirty_thresh);
  49.         if (bdi_thresh < 2 * bdi_stat_error(bdi)) {
  50.             bdi_reclaimable = bdi_stat_sum(bdi, BDI_RECLAIMABLE);
  51.             bdi_dirty = bdi_reclaimable +
  52.                     bdi_stat_sum(bdi, BDI_WRITEBACK);
  53.         } else {
  54.             //bdi脏页数
  55.             bdi_reclaimable = bdi_stat(bdi, BDI_RECLAIMABLE);
  56.             //bdi脏页数+bdi正在回写的脏页数
  57.             bdi_dirty = bdi_reclaimable +
  58.                     bdi_stat(bdi, BDI_WRITEBACK);
  59.         }
  60.         /*bdi_dirty nr_dirty 都超标则dirty_exceeded=1bdi_dirtynr_dirty有啥区别呢?测试脏页平衡休眠时,有时dirty_exceeded1,有时dirty_exceeded0*/
  61.         dirty_exceeded = (bdi_dirty > bdi_thresh) &&
  62.                   (nr_dirty > dirty_thresh);
  63.         //如果脏页超标则bdi->dirty_exceeded=1
  64.         if (dirty_exceeded && !bdi->dirty_exceeded)
  65.             bdi->dirty_exceeded = 1;
  66.         //计算bdi->write_bandwidthbdi->dirty_ratelimit,过程很复杂
  67.         bdi_update_bandwidth(bdi, dirty_thresh, background_thresh,
  68.                      nr_dirty, bdi_thresh, bdi_dirty,
  69.                      start_time);
  70.         dirty_ratelimit = bdi->dirty_ratelimit;
  71.         //pos_ratio的计算是个玄学,与脏页数有关系。全局pos_ratio介于0~2之间,系统脏页nr_dirty越多pos_ratio越小,nr_dirty超过最大脏页阀值pos_ratio0,这表示系统脏页太多了,进程要立即进行脏页平衡而休眠?
  72.         pos_ratio = bdi_position_ratio(bdi, dirty_thresh,
  73.                            background_thresh, nr_dirty,
  74.                            bdi_thresh, bdi_dirty);
  75.        
  76.         //根据dirty_ratelimitpos_ratio计算进程的task_ratelimittask_ratelimit用来限制进程因脏页太多而休眠的时间task_ratelimit = dirty_ratelimit *(pos_ratio/1024),pos_ratio/10240~2左右,是个比例值
  77.         task_ratelimit = ((u64)dirty_ratelimit * pos_ratio) >>
  78.                             RATELIMIT_CALC_SHIFT;
  79.         //根据bdi_dirty计算最大休眠时间mac_pausebdi_dirty越大休眠时间越长
  80.         max_pause = bdi_max_pause(bdi, bdi_dirty);
  81.         //根据max_pausetask_ratelimitdirty_ratelimit计算最小休眠时间
  82.         min_pause = bdi_min_pause(bdi, max_pause,
  83.                       task_ratelimit, dirty_ratelimit,
  84.                       &nr_dirtied_pause);//进程脏页数超过nr_dirtied_pause就要进行脏页平衡
  85.         //测试task_ratelimit会出现0,但是概率很低,此时脏页太多必须休眠
  86.         if (unlikely(task_ratelimit == 0)) {
  87.             period = max_pause;
  88.             pause = max_pause;
  89.             goto pause;
  90.         }
  91.         //period=当前进程脏页数pages_dirtied除以task_ratelimit,因脏页太多而要休眠的时间,与脏页数有关。与HZ相乘后,单位就和jiffies一样了
  92.         period = HZ * pages_dirtied / task_ratelimit;
  93.         //pause很重要,表示当前进程因为脏页太多而被迫休眠的时间
  94.         pause = period;
  95.      //current->dirty_paused_when是进程上一次执行balance_dirty_pages()脏页平衡的时间点(或者再加上休眠的时间点)now-current->dirty_paused_when这个时间差可能为正或者负数。时间差为正数时减少pause休眠时间,时间差为负数时增大pause休眠时间。
  96.         if (current->dirty_paused_when)
  97.             pause -= now - current->dirty_paused_when;
  98.         //pause小于最小休眠时间min_pause直接break,休眠时间太小干脆不休眠了
  99.         if (pause < min_pause) {
  100.             ...............
  101.             if (pause < -HZ) {
  102.                 //current->dirty_paused_when太小被更新为当前时间
  103.                 current->dirty_paused_when = now;
  104.                 //每次执行balance_dirty_pages()函数都对current->nr_dirtied0,这表示进行了脏页平衡所以对清0???????
  105.                 current->nr_dirtied = 0;
  106.             } else if (period) {//period >=-HZ且不为0
  107.                 current->dirty_paused_when += period;//current->dirty_paused_when
  108.                 //每次执行balance_dirty_pages()函数都对current->nr_dirtied0,这表示进行了脏页平衡所以对清0???????
  109.                 current->nr_dirtied = 0;
  110.             //这里在period0时成立,
  111.             } else if (current->nr_dirtied_pause <= pages_dirtied)
  112.                 //current->nr_dirtied_pause累加当前进程的脏页数pages_dirtied,越来越大
  113.                 current->nr_dirtied_pause += pages_dirtied;
  114.            
  115.             break;
  116.         }
  117.         //pause超过最大休眠时间则被赋值max_pause
  118.         if (unlikely(pause > max_pause)) {
  119.             /* for occasional dropped task_ratelimit */
  120.             now += min(pause - max_pause, max_pause);
  121.             pause = max_pause;
  122.         }
  123. pause:
  124.         __set_current_state(TASK_KILLABLE);
  125.         //休眠pause毫秒
  126.         io_schedule_timeout(pause);
  127.         //current->dirty_paused_when这里记录脏页balance_dirty_pages的时间=当前时间+休眠时间,pause最大200ms。如果系统脏页太多,进程很快又执行到balance_dirty_pages()里的pause -= now-current->dirty_paused_when ,计算进程因为脏页太多休眠的时间,会出现now-current->dirty_paused_when是负数,导致pause很大,进程又要休眠。这样的目的应该是系统脏页太多了,进程多休眠。
  128.         current->dirty_paused_when = now + pause;
  129.         //每次执行balance_dirty_pages()函数都对current->nr_dirtied0,这表示进行了脏页平衡所以对清0???????
  130.         current->nr_dirtied = 0;
  131.         current->nr_dirtied_pause = nr_dirtied_pause;//脏页太多休眠唤醒后current->nr_dirtied_pause赋初值
  132.         ............
  133.     }
  134.     //脏页不超标了则对bdi->dirty_exceeded0。这里有个理解误区,当进程1balance_dirty_pages()函数前边bdi->dirty_exceeded=11,但是进程1执行到这里不会对bdi->dirty_exceeded=00,因为dirty_exceeded1。需另外的进程执行到这里才会bdi->dirty_exceeded=00
  135.     if (!dirty_exceeded && bdi->dirty_exceeded)
  136.         bdi->dirty_exceeded = 0;
  137.     ............
  138. }

这个流程有点繁琐,为了能突出脏页回写的重点,这里先把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_ratiopos_ratio>>RATELIMIT_CALC_SHIFT介于0~2左右,是个比例值。接着执行task_ratelimit = ((u64)dirty_ratelimit * pos_ratio) >>RATELIMIT_CALC_SHIFT计算task_ratelimit,相当于令dirty_ratelimit乘以一个0~2之间的数字得到task_ratelimitbdibdi->dirty_ratelimitpos_ratiotask_ratelimit计算流程也相当复杂。测试时发现,bdi->dirty_ratelimit在一段时间(200ms)是个固定值。
  • 8 执行max_pause = bdi_max_pause(bdi, bdi_dirty)根据bdi_dirty计算进程因脏页平衡最大休眠时间。执行min_pause = bdi_min_pause(bdi, max_pausetask_ratelimit, dirty_ratelimit, &nr_dirtied_pause)根据计算max_pausetask_ratelimitdirty_ratelimit进程因脏页平衡最小休眠时间。max_pausemin_pause的结果与系统脏页数、task_ratelimitdirty_ratelimit紧密相关。
  • 9 执行 period = HZ * pages_dirtied / task_ratelimitpause = 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_whencurrent->dirty_paused_when等于当前系统时间加上休眠时间。还执行current->nr_dirtied = 0对当前进程脏页数清0,这里需要说明下,只要执行balance_dirty_pages()函数都对current->nr_dirtied0(该函数有多处),这表示只要进行了脏页平衡就对current->nr_dirtied0(我猜测的)。最后执行current->nr_dirtied_pause = nr_dirtied_pause更新当前进程进行脏页平衡的阀值。

我觉得balance_dirty_pages()函数最核心的一点就是计算进程因脏页平衡而休眠的时间pause。这里再总结一下这个流程:

  • 1 计算系统脏页数nr_dirtybdi脏页数bdi_dirty,计算脏页相关阀值background_threshdirty_threshbdi_thresh
  • 2 根据nr_dirtybdi_dirtybackground_threshdirty_threshbdi_thresh计算bdi->dirty_ratelimitpos_ratiotask_ratelimit。计算因脏页平衡而休眠的最小和最大休眠时间min_pausemax_pause
  • 3 根据当前进程脏页数pages_dirtied(current->nr_dirtied)task_ratelimit计算因脏页平衡而休眠的时间pause。还会执行pause -= now - current->dirty_paused_whenpause做一定调整。之后如果pause>=min_pause则当前进程就要执行io_schedule_timeout(pause)休眠pause毫秒。

说实话这个流程很繁琐,尤其是bdi->dirty_ratelimitpos_ratiotask_ratelimit的计算过程更甚。看源码涉及到了一些数学思想,计算原理还不太清楚。这3个变量控制进程脏页平衡而休眠的时间,系统认为当前进程需要休眠而等待脏页刷回磁盘,就要休眠。这里把计算过程再梳理下:

1.3 bdi->dirty_ratelimit、task_ratelimit更深层次讲解

首先提一点,每个块设备都有一个struct backing_dev_info *bdi结构体,其成员bdi->avg_write_bandwidthbdi->write_bandwidthbdi->dirty_ratelimitbdi->balanced_dirty_ratelimit都与脏页平衡有关。他们的初值都是INIT_BW,在bdi块设备初始化中赋值INIT_BW,这是100M对应的page数。

  1. #define INIT_BW     (100 << (20 - PAGE_SHIFT))// 100M对应的page
  2. int bdi_init(struct backing_dev_info *bdi)
  3. {
  4.     bdi->balanced_dirty_ratelimit = INIT_BW;
  5.     bdi->dirty_ratelimit = INIT_BW;
  6.     bdi->write_bandwidth = INIT_BW;
  7.     bdi->avg_write_bandwidth = INIT_BW;
  8. }

感觉bdi->avg_write_bandwidthbdi->write_bandwidthbdi->dirty_ratelimitbdi->balanced_dirty_ratelimit表示的是脏页有关的page数。在balance_dirty_pages->bdi_update_bandwidth->__bdi_update_bandwidth函数中计算更新这4个参数,看下源码:

  1. void __bdi_update_bandwidth(struct backing_dev_info *bdi,
  2.                 unsigned long thresh,
  3.                 unsigned long bg_thresh,
  4.                 unsigned long dirty,
  5.                 unsigned long bdi_thresh,
  6.                 unsigned long bdi_dirty,
  7.                 unsigned long start_time)
  8. {
  9.     unsigned long now = jiffies;
  10.     //bdi->bw_time_stamp是上次执行__bdi_update_bandwidth()计算bdi->dirty_ratelimit的系统时间,相减后elapsed需要大于200ms,才能再次计算bdi->dirty_ratelimit
  11.     unsigned long elapsed = now - bdi->bw_time_stamp;
  12.     unsigned long dirtied;
  13.     unsigned long written;
  14.     //每两次的时间间隔要大于200ms,否则直接返回
  15.     if (elapsed < BANDWIDTH_INTERVAL)
  16.         return;
  17.     //当前bdi块设备的脏页数
  18.     dirtied = percpu_counter_read(&bdi->bdi_stat[BDI_DIRTIED]);
  19.     //当前bdi块设备已经回写的脏页数
  20.     written = percpu_counter_read(&bdi->bdi_stat[BDI_WRITTEN]);
  21.     if (elapsed > HZ && time_before(bdi->bw_time_stamp, start_time))
  22.         goto snapshot;
  23.     if (thresh) {
  24.         global_update_bandwidth(thresh, dirty, now);
  25.         //里边计算更新 bdi->dirty_ratelimit bdi->balanced_dirty_ratelimit
  26.         bdi_update_dirty_ratelimit(bdi, thresh, bg_thresh, dirty,
  27.                        bdi_thresh, bdi_dirty,
  28.                        dirtied, elapsed);
  29.     }
  30.    
  31.     //这里计算 bdi->write_bandwidth bdi->avg_write_bandwidth
  32.     bdi_update_write_bandwidth(bdi, elapsed, written);
  33. snapshot:
  34.     bdi->dirtied_stamp = dirtied;//保存本次bdi块设备的脏页数
  35.     bdi->written_stamp = written;//保存本次bdi块设备已经回写的脏页数
  36.     bdi->bw_time_stamp = now;//记录 __bdi_update_bandwidth()函数中更新bdi->dirty_ratelimit的时间
  37. }

先看下bdi_update_write_bandwidth()函数:根据最近一段时间内bdi块设备回写的脏页数,计算bdi->write_bandwidthbdi->avg_write_bandwidth。最近一段时间内bdi块设备回写的脏页数越多,bdi->write_bandwidthbdi->avg_write_bandwidth越大,实际计算原理很复杂,源码如下:

  1. static void bdi_update_write_bandwidth(struct backing_dev_info *bdi,
  2.                        unsigned long elapsed,
  3.                        unsigned long written)//written是当前bdi块设备已经回写的脏页数
  4. {
  5.     const unsigned long period = roundup_pow_of_two(3 * HZ);
  6.     unsigned long avg = bdi->avg_write_bandwidth;
  7.     unsigned long old = bdi->write_bandwidth;
  8.     u64 bw;
  9.    
  10.     //bdi->written_stamp是上次执行__bdi_update_bandwidth()bdi块设备回写脏页数,written是现在bdi块设备回写脏页数。二者相减是计算最近两次时间间隔内bdi块设备回写的脏页数
  11.     bw = written - min(written, bdi->written_stamp);
  12.     bw *= HZ;
  13.     if (unlikely(elapsed > period)) {
  14.         do_div(bw, elapsed);
  15.         avg = bw;
  16.         goto out;
  17.     }
  18.     //就是 bw * elapsed + write_bandwidth * (period - elapsed)
  19.     bw += (u64)bdi->write_bandwidth * (period - elapsed);
  20.     //就是 (bw * elapsed + write_bandwidth * (period - elapsed))/period
  21. bw >>= ilog2(period);
  22.     //avg > old >bw,则avg减少(avg - old)/8,使得avg接近bw
  23.     if (avg > old && old >= (unsigned long)bw)
  24.         avg -= (avg - old) >> 3;
  25.     //avg < old < bw,则avg增加(avg - old)/8,使得avg接近bw
  26.     if (avg < old && old <= (unsigned long)bw)
  27.         avg += (old - avg) >> 3;
  28. out:
  29.     //bdi->write_bandwidth大致就表示这段时间单位时间内bdi块设备回写磁盘的脏页数吧?
  30.     bdi->write_bandwidth = bw;
  31.     //bdi->avg_write_bandwidth缓慢接近bdi->write_bandwidth
  32.     bdi->avg_write_bandwidth = avg;
  33. }  

接着看下bdi_update_dirty_ratelimit()函数,计算bdi->dirty_ratelimitbdi->balanced_dirty_ratelimit

  1. static void bdi_update_dirty_ratelimit(struct backing_dev_info *bdi,
  2.                        unsigned long thresh,
  3.                        unsigned long bg_thresh,
  4.                        unsigned long dirty,
  5.                        unsigned long bdi_thresh,
  6.                        unsigned long bdi_dirty,
  7.                        unsigned long dirtied,
  8.                        unsigned long elapsed)
  9. {
  10.     //freerun=(thresh+bg_thresh)/2 最小脏页阀值
  11.     unsigned long freerun = dirty_freerun_ceiling(thresh, bg_thresh);
  12.     //limit是最大脏页阀值
  13.     unsigned long limit = hard_dirty_limit(thresh);
  14.     //setpointfreerunlimit取半
  15.     unsigned long setpoint = (freerun + limit) / 2;
  16.     //write_bw=bdi->write_bandwidth大致表示前一次单位时间内bdi块设备回写磁盘的脏页数
  17.     unsigned long write_bw = bdi->avg_write_bandwidth;
  18.     unsigned long dirty_ratelimit = bdi->dirty_ratelimit;//前一次bdi块设备脏页限制速率
  19.     unsigned long dirty_rate;
  20.     unsigned long task_ratelimit;
  21.     unsigned long balanced_dirty_ratelimit;
  22.     unsigned long pos_ratio;
  23.     unsigned long step;
  24.     unsigned long x;
  25.     //dirtiedbdi块设备当前的脏页数,bdi->dirtied_stamp是前一次执行bdi_update_dirty_ratelimit()计算bdi->dirty_ratelimit时的脏页数,elapsed这两次的时间差。二者相除计算出来的dirty_rate就是这段时间内该bdi块设备产生的脏页数,就是脏页产生速率吧。再乘以HZ是为了跟系统时间扯上关系吧,本质没啥意义。
  26.     dirty_rate = (dirtied - bdi->dirtied_stamp) * HZ / elapsed;
  27.     //pos_ratio的计算是个玄学,与脏页数有关系。全局pos_ratio介于0~2之间,系统脏页nr_dirty越多pos_ratio越小,nr_dirty超过最大脏页阀值pos_ratio0,这表示系统脏页太多了,进程要立即进行脏页平衡而休眠。bdi pos_ratio的计算是个玄学,搞不清楚
  28.     pos_ratio = bdi_position_ratio(bdi, thresh, bg_thresh, dirty,
  29.                        bdi_thresh, bdi_dirty);
  30.     //dirty_ratelimit是上一次计算的 bdi->dirty_ratelimit,从而计算出task_ratelimit
  31.     task_ratelimit = (u64)dirty_ratelimit *pos_ratio >> RATELIMIT_CALC_SHIFT;
  32.     .........
  33.     //write_bw与前一次单位时间内bdi块设备回写磁盘的脏页数有关,dirty_rate是前一次到这次时间内刷回磁盘的脏页数。balanced_dirty_ratelimit = task_ratelimit *(write_bw/dirty_rate),按照内核说明,这是为了计算对一个进程的脏页速率限制数,搞不清楚,dirty_rate跟写文件的进程数有啥关系???????
  34.     balanced_dirty_ratelimit = div_u64((u64)task_ratelimit * write_bw,
  35.                        dirty_rate | 1);
  36.     //balanced_dirty_ratelimit最大不能超过write_bwwrite_bw与前一次计算的单位时间内bdi块设备回写磁盘的脏页数有关。就是说本次计算出来的balanced_dirty_ratelimit脏页平衡脏页速率限制page数,不能超过前一次单位时间内bdi块设备回写磁盘的脏页数,这是什么逻辑?
  37.     if (unlikely(balanced_dirty_ratelimit > write_bw))
  38.         balanced_dirty_ratelimit = write_bw;
  39.     .............
  40.     //dirty_ratelimit是在上一次的基础上增加或者减少step,从而更接近本次计算的balanced_dirty_ratelimit
  41.     if (dirty_ratelimit < balanced_dirty_ratelimit)
  42.         dirty_ratelimit += step;
  43.     else
  44.         dirty_ratelimit -= step;
  45.     bdi->dirty_ratelimit = max(dirty_ratelimit, 1UL);
  46.     bdi->balanced_dirty_ratelimit = balanced_dirty_ratelimit;
  47. }  

这个计算过程涉及的数学思想挺负责的。我们只需理解一点,bdi->balanced_dirty_ratelimit跟脏页产生速率和脏页回写速率有关,bdi->dirty_ratelimit接近bdi->balanced_dirty_ratelimit。也就是说bdi->dirty_ratelimit的计算原理很复杂,它与该bdi块设备的脏页产生速率和脏页回写速率有关,很复杂。之后再根据bdi->dirty_ratelimit计算task_ratelimitmax_pausemin_pause、脏页平衡休眠的时间period。再看下这个流程:

  1. static void balance_dirty_pages(struct address_space *mapping,
  2.                 unsigned long pages_dirtied)//pages_dirtied=current->nr_dirtied,当前进程脏页数
  3. {
  4.     ..................
  5.     //计算bdi->write_bandwidthbdi->dirty_ratelimit,过程很复杂
  6.     bdi_update_bandwidth(bdi, dirty_thresh, background_thresh,
  7.                  nr_dirty, bdi_thresh, bdi_dirty,
  8.                  start_time);   
  9.     //pos_ratio的计算是个玄学,与脏页数有关系。全局pos_ratio介于0~2之间,系统脏页nr_dirty越多pos_ratio越小,nr_dirty超过最大脏页阀值pos_ratio0,这表示系统脏页太多了,进程要立即进行脏页平衡而休眠?
  10.     pos_ratio = bdi_position_ratio(bdi, dirty_thresh,
  11.                            background_thresh, nr_dirty,
  12.                            bdi_thresh, bdi_dirty);
  13.     //根据dirty_ratelimitpos_ratio计算进程的task_ratelimittask_ratelimit用来限制进程因脏页太多而休眠的时间。本质是task_ratelimit = dirty_ratelimit *(pos_ratio/1024)pos_ratio/10240~2左右,是个比例值
  14.     task_ratelimit = ((u64)dirty_ratelimit * pos_ratio) >>
  15.                             RATELIMIT_CALC_SHIFT;
  16.                            
  17.     //根据bdi_dirty计算最大休眠时间max_pause
  18.     max_pause = bdi_max_pause(bdi, bdi_dirty);
  19.     //根据max_pausetask_ratelimitdirty_ratelimit计算最小休眠时间
  20.     min_pause = bdi_min_pause(bdi, max_pause,
  21.                   task_ratelimit, dirty_ratelimit,
  22.                   &nr_dirtied_pause);//进程脏页数超过nr_dirtied_pause就要阻塞休眠等待脏页回写
  23.     .............                
  24.     //period=当前进程脏页数pages_dirtied除以task_ratelimit,因脏页太多而最大休眠时间
  25.     period = HZ * pages_dirtied / task_ratelimit;
  26. }

简单来说,先根据当前bdi块设备脏页产生速率和脏页回写速率,经过繁琐的计算得到bdi->dirty_ratelimit,再计算出pos_ratio,根据二者计算出task_ratelimittask_ratelimit感觉是为了限制进程产生脏页速率的,怎么限制呢?就是让进程休眠!接着,根据进程脏页数period = HZ * pages_dirtied / task_ratelimit计算进程因脏页平衡要休眠的时间pages_dirtied是进程脏页数,显然进程脏页数越多period越大。并且bdi->dirty_ratelimitbdi_update_dirty_ratelimit()中计算出已经与HZ挂钩,task_ratelimit当然也是,因此period = HZ * pages_dirtied / task_ratelimitHZ就会抵消掉。

bdi_max_pausebdi_min_pause的计算过程如下,计算也是个玄学

  1. static long bdi_min_pause(struct backing_dev_info *bdi,
  2.               long max_pause,
  3.               unsigned long task_ratelimit,
  4.               unsigned long dirty_ratelimit,
  5.               int *nr_dirtied_pause)
  6. {
  7.     .........
  8.     t = max(1, HZ / 100);//t这里表示进程因脏页平衡而休眠的时间,初值10ms,正常min_pause=t/2
  9.     //这里计算出来pages就是nr_dirtied_pause,进程要进行脏页平衡的脏页阀值,与dirty_ratelimit这个脏页平衡限制速率page数和因脏页平衡而休眠的时间t成正比。
  10.     pages = dirty_ratelimit * t / roundup_pow_of_two(HZ);
  11.     .........
  12.     //进程要进行脏页平衡的脏页上限
  13.     *nr_dirtied_pause = pages;
  14.     //t这里表示进程因脏页平衡而休眠的时间,nr_dirtied_pause大于128page时,min_pause=t/2,否则min_pause=t.nr_dirtied_pause
  15.     return pages >= DIRTY_POLL_THRESH ? 1 + t / 2 : t;
  16. }
  17. static unsigned long bdi_max_pause(struct backing_dev_info *bdi,
  18.                    unsigned long bdi_dirty)
  19. {
  20.     unsigned long bw = bdi->avg_write_bandwidth;
  21.     unsigned long t;
  22.     //bdi_dirty脏页越多,bw与前一次单位时间内bdi块设备回写磁盘的脏页数有关,bw越少,计算出max_pause越大。就是说,脏页越多而bdi块设备回写磁盘的脏页数太少,则因脏页平衡而休眠的时间应该越大??????
  23.     t = bdi_dirty / (1 + bw / roundup_pow_of_two(1 + HZ / 8));
  24.     t++;
  25.     return min_t(unsigned long, t, MAX_PAUSE);
  26. }

脏页平衡的计算到处是玄学(准确说是数学)最后再把几个隐藏知识点列下:

系统脏页nr_dirtybdi脏页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_threshnr_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 = 00

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

  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
timer_interrupt() 是 Linux 内核中的一个定时器中断处理函数,它主要用于处理内核中的各种定时器事件,包括时钟中断、定时器任务等。 源码如下: ```c void __irqentry timer_interrupt(struct pt_regs *regs) { ... update_process_times(user_mode(regs)); profile_tick(CPU_PROFILING, user_mode(regs), regs); if (user_mode(regs)) return; irq_enter(); #ifdef CONFIG_NO_HZ_COMMON /* * The tick broadcast device is disabled after the first * CPU goes offlined, see tick_nohz_enable. */ if (tick_nohz_tick_stopped()) goto out; #endif tick_check_idle(TICK_NMI_SAFE); /* * Tickless idle is in progress. */ if (idle_cpu(smp_processor_id())) { watchdog_touch(); smp_idle_check(); goto out; } /* * Check if we need to do anything at all: */ if (!tick_check_oneshot_broadcast(tick_nohz_full_cpu_idle())) { if (tick_check_oneshot() && !tick_check_broadcast_expired() && !tick_broadcast_oneshot_active()) { tick_program_event(tick_oneshot_broadcast, oneshot_timer.expires); goto out; } if (tick_check_broadcast_spurious()) goto out; if (tick_check_cpu_dead(cpu) || tick_check_new_device(cpu)) goto out; tick_check_replacement(cpu); } /* * Re-enable periodic tick if it is stopped and there are no * oneshot or broadcast events pending: */ if (tick_check_periodic() && !tick_check_oneshot_active() && !tick_check_broadcast_active()) tick_program_event(tick_periodic, tick_next_period); out: irq_exit(); ... } ``` 该函数的主要流程如下: 1. 调用 update_process_times() 和 profile_tick() 更新进程的时间信息和性能分析信息。 2. 判断是否是用户态,如果是则直接返回。 3. 调用 irq_enter() 进入中断上下文。 4. 检查 tickless idle 是否正在进行,如果是,则直接返回。 5. 检查是否正在进行 idle,如果是,则调用 watchdog_touch() 和 smp_idle_check(),并直接返回。 6. 检查是否需要进行任何操作。 7. 如果需要,检查是否需要启动一次性定时器事件。 8. 如果需要,检查是否需要启动广播定时器事件。 9. 如果需要,检查是否需要停止定时器,并重新启动。 10. 调用 irq_exit() 退出中断上下文。 总的来说,timer_interrupt() 函数主要用于检查和处理各种定时器事件,以保证内核的正常运行。这些事件包括一次性定时器、广播定时器、周期性定时器等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值