Bootstrap

探究 sleep(0) 和 sched_yield()

先放结论:

  • sleep(0) 和 sched_yield() 都能起到让系统调度的作用
  • 如果业务吞吐量不高,且需要降低CPU使用率时,可以使用 sleep(0)
  • 在需要保证响应速度的情况下,可以使用 sched_yield()

问题起因

在使用 epoll_wait 时,发现运行期间,CPU的使用率达到了 100%,风扇一直响。
如果长时间没有就绪事件,其实就相当于出现了 busy wait。这种情况下需要及时的让权,以给予其他进程更多的运行机会,常用的方案有两种:

  • sleep(0)
  • sched_yield()

sched_yield() 其实好理解,就是让进程显式地将处理器时间让给其他等待执行进程的机制。它是通过将进程从活动队列移动到过期队列中实现的。由于实时进程不会过期,因此,只是将其移动到活动队列的最后面。

其实,sleep(0) 在 windows 中经常使用,MSDN这样解释

A value of zero causes the thread to relinquish the remainder of its time slice to any other thread that is ready to run. If there are no other threads ready to run, the function returns immediately, and the thread continues execution.

那么问题来了:

  • 在 Linux 中 sleep(0) 是否也能起到相应作用?
  • sleep(0) 和 sched_yield() 的区别是什么?

测试

使用如下代码测试 sleep(0) 和 sched_yield() 的作用。运行时间是测试运行 1e5 次的所需时间。CPU 使用率是在while(1) 循环下测得。
以下数据均为同一台电脑测试得到。

#include <unistd.h>
#include <sched.h>

int main(void) 
{
    int i;
    // for (i = 0; i < (int)(1e5); ++i) {
    //     // sleep(0);
    //     sched_yield();
    // }
    while (1) {
        // sleep(0);
        sched_yield();
    }
    return 0;
}

结果如下:

方案1e5次的运行时间CPU 使用率
sleep(0)9.501s38.5%
sched_yield()0.080s99.7%
空语句0.006s99.7%

从上表可以看出,sleep(0)运行时间比sched_yield() 多出两个数量级。

  • sleep(0) 是否引起了系统调度?
  • 为什么 sleep(0) 会比 sched_yield() 慢这么多?

通过 trace 程序的具体执行过程来解释问题

这里使用 trace-cmd,他可以跟踪系统调用。
通过以下命令来查看具体的执行过程

ps -aux | grep a.out
sudo trace-cmd record -P 12345 -p function_graph
sudo trace-cmd report | less

两种情况的执行过程如下:

sleep(0) 的单次执行过程。

为了方便观看,省略了一部分子过程。
trace report 的结果里,会在大于10微秒前添加 “+” 号,在大于100微秒的前面添加 “!” 号。
可以看到:

  • sleep 的执行实际是调用了 hrtimer_nanosleep()。即使参数是0,也完整调用了整个过程,没有像windows 中的那样对 0 值进行优化,耗时明显。
  • sleep 过程中触发了系统的调度。但是系统调度会将进程从红黑树中移出,并放入等待队列。这个过程耗时明显。
  • 调度更像是进程休眠之后造成的结果。
              |  __x64_sys_clock_nanosleep() {
              |    common_nsleep() {
              |      hrtimer_nanosleep() {
              |        do_nanosleep() {
              |          hrtimer_start_range_ns() {
 + 35.521 us  |          }
              |          schedule() {
              |            rcu_note_context_switch() {
   0.119 us   |              rcu_qs();
   0.344 us   |            }
   0.116 us   |            _raw_spin_lock();
   0.158 us   |            update_rq_clock();
              |            psi_task_change() {
   4.604 us   |            }
              |            dequeue_task_fair() {
              |              dequeue_entity() {
              |                update_curr() {
   0.122 us   |                  update_min_vruntime();
              |                  cpuacct_charge() {
   0.115 us   |                    rcu_read_unlock_strict();
   0.400 us   |                  }
              |                  __cgroup_account_cputime() {
   0.108 us   |                    cgroup_rstat_updated();
   0.337 us   |                  }
   0.105 us   |                  rcu_read_unlock_strict();
   1.690 us   |                }
   0.113 us   |                clear_buddies();
   0.109 us   |                update_cfs_group();
   0.649 us   |                update_min_vruntime();
 + 34.699 us  |              }
   0.113 us   |              hrtick_update();
 + 37.848 us  |            }
              |            pick_next_task_fair() {
   0.111 us   |              check_cfs_rq_runtime();
              |              pick_next_entity() {
   0.111 us   |                clear_buddies();
   0.345 us   |              }
              |              pick_next_entity() {
   0.111 us   |                clear_buddies();
   0.335 us   |              }
              |              put_prev_entity() {
   0.109 us   |                check_cfs_rq_runtime();
   0.345 us   |              }
              |              set_next_entity() {
   1.432 us   |              }
              |              put_prev_entity() {
   0.111 us   |                check_cfs_rq_runtime();
   0.333 us   |              }
              |              set_next_entity() {
   0.948 us   |              }
   4.844 us   |            }
              |            psi_task_switch() {
 + 13.316 us  |            }
 ! 227.672 us |          }
 ! 264.588 us |        }
 ! 265.359 us |      }
 ! 265.606 us |    }
 ! 266.468 us |  }

sched_yield() 的单次执行过程。

该子过程没有删减。只有一个schedule(), 整个过程简洁清晰。

             |  __do_sys_sched_yield() {
             |    do_sched_yield() {
  0.132 us   |      _raw_spin_lock();
  0.128 us   |      yield_task_fair();
             |      schedule() {
             |        rcu_note_context_switch() {
  0.125 us   |          rcu_qs();
  0.382 us   |        }
  0.125 us   |        _raw_spin_lock();
  0.139 us   |        update_rq_clock();
             |        pick_next_task_fair() {
             |          update_curr() {
  0.123 us   |            update_min_vruntime();
  0.382 us   |          }
  0.129 us   |          check_cfs_rq_runtime();
             |          pick_next_entity() {
  0.133 us   |            clear_buddies();
  0.413 us   |          }
             |          update_curr() {
  0.122 us   |            update_min_vruntime();
             |            cpuacct_charge() {
  0.131 us   |              rcu_read_unlock_strict();
  0.396 us   |            }
             |            __cgroup_account_cputime() {
  0.129 us   |              cgroup_rstat_updated();
  0.380 us   |            }
  0.128 us   |            rcu_read_unlock_strict();
  1.695 us   |          }
  0.126 us   |          check_cfs_rq_runtime();
             |          pick_next_entity() {
  0.140 us   |            clear_buddies();
  0.407 us   |          }
  4.056 us   |        }
  5.350 us   |      }
  6.127 us   |    }
  6.485 us   |  }

结论

  • sleep(0) 和 sched_yield() 都能起到让系统调度的作用,sleep(0) 更耗时
  • 如果业务吞吐量不高,且需要降低CPU使用率时,可以使用 sleep(0)
  • 在需要保证响应速度的情况下,使用 sched_yield()

参考:

;