从一个CFS调度案例谈Linux系统卡顿的根源

Linux系统是一个让人感觉卡顿的系统 , 卡顿的原因在于Linux内核的调度器从来不关注业务场景!
Linux内核只能看到机器而不愿意看到应用 。 它倾向于自下而上从CPU角度提高吞吐 , 而不是自上而下从业务角度提高用户体验 。
拟人来看 , Linux是一个好程序员 , 但不是一个好经理 。
万事必有因缘 , Linux就是一个程序员发起一帮程序员折腾起来的 , 几乎没有穿西装的经理之类的人参与 。
程序员天天挂在嘴边的就是性能 , 时间复杂度 , cache利用率 , CPU , 内存 , 反之 , 经理每天吆喝的就是客户 , 客户 , 客户 , 体验 , 体验 , 体验!
最近有朋友问了我一个问题 , 说是他在调试一个消息队列组件 , 涉及到生产者 , 消费者等多个相互配合的线程 , 非常复杂 , 部署上线后 , 发现一个奇怪的问题:

  • 整个系统资源似乎被该组件的线程独占 , 该消息队列组件的效率非常高 , 但其系统非常卡顿!
我问他有没有部署cgroup , cpuset之类的配置 , 他说没有 。
我又问他该消息队列组件一共有多少线程 , 他说不多 , 不超过20个 。
我又问…他说…

我感到很奇怪 , 我告诉他说让我登录机器调试下试试看 , 他并没有同意 , 只是能尽可能多的告诉我细节 , 我来远程协助 。

我并不懂消息队列 , 我也不懂任何的中间件 , 调试任何一个此类系统对我而言是无能为力的 , 我也感到遗憾 , 我只能尝试从操作系统的角度去解释和解决这个问题 。 很显然 , 这就是操作系统的问题 。 而且很明确 , 这是操作系统的调度问题 。
涉及生产者 , 消费者 , 如果我能本地重现这个问题 , 那么我就一定能解决这个问题 。
于是开始Google关于Linux schedule子系统关于生产者和消费者的内容 , producer , consumer , schedule , cfs , linux kernel…我找到了下面的patch:
Impact: improve/change/fix wakeup-buddy schedulingCurrently we only have a forward looking buddy, that is, we prefer toschedule to the task we last woke up, under the presumption that itsgoing to consume the data we just produced, and therefore will havecache hot benefits.This allows co-waking producer/consumer task pairs to run ahead of thepack for a little while, keeping their cache warm. Without this, wewould interleave all pairs, utterly trashing the cache.This patch introduces a backward looking buddy, that is, suppose thatin the above scenario, the consumer preempts the producer before itcan go to sleep, we will therefore miss the wakeup from consumer toproducer (its already running, after all), breaking the cycle andreverting to the cache-trashing interleaved schedule pattern.The backward buddy will try to schedule back to the task that woke usup in case the forward buddy is not available, under the assumptionthat the last task will be the one with the most cache hot task aroundbarring current.This will basically allow a task to continue after it got preempted.In order to avoid starvation, we allow either buddy to get wakeup_granahead of the pack.
似乎CFS调度器的LAST_BUDDY feature相关 , 该feature涉及运行队列的next , last指针 , 好不容易找到了这个 , 我决定设计一个最简单的实验 , 尝试复现问题并对LAST_BUDDY feature进行一番探究 。
我的实验非常简单:
  • 生产者循环唤醒消费者 。
为了让唤醒操作更加直接 , 我希望采用wake_up_process直接唤醒 , 而不是使用信号这些复杂的机制 , 所以我必须寻找内核的支持 , 首先写一个字符设备来支持这个操作:
// wakedev.c#include #include #include #include #include #include#define CMD_WAKE 122dev_t dev = 0;static struct class *dev_class;static struct cdev wake_cdev;static long _ioctl(struct file *file, unsigned int cmd, unsigned long arg);static struct file_operations fops = { .owner= THIS_MODULE, .unlocked_ioctl = _ioctl,};static long _ioctl(struct file *file, unsigned int cmd, unsigned long arg){ u32 pid = 0; struct task_struct *task = NULL; switch(cmd) {// ioctl唤醒命令case CMD_WAKE:copy_from_user(task = pid_task(find_vpid(pid), PIDTYPE_PID);if (task) {wake_up_process(task);}break; } return 0;}static int __init crosswake_init(void){ if((alloc_chrdev_region(return -1; } printk("major=%d minor=%d \n",MAJOR(dev), MINOR(dev)); cdev_init( if ((cdev_add(goto err_class; } if ((dev_class = class_create(THIS_MODULE, "etx_class")) == NULL) {printk("class failed\n");goto err_class; } if ((device_create(dev_class, NULL, dev, NULL, "etx_device")) == NULL) {printk(KERN_INFO "create failed\n");goto err_device; } return 0;err_device: class_destroy(dev_class);err_class: unregister_chrdev_region(dev,1); return -1;}void __exit crosswake_exit(void){ device_destroy(dev_class,dev); class_destroy(dev_class); cdev_del( unregister_chrdev_region(dev, 1);}module_init(crosswake_init);module_exit(crosswake_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("shabi");